HTTP Request Execution
Section titled “HTTP Request Execution”The hoody-curl service exposes a small set of HTTP endpoints for executing libcurl-driven requests and observing their lifecycle. The two request endpoints (POST and GET) cover most execution needs, from one-shot fetches with full cURL configuration to background jobs with retries, sessions, and response storage. The SSE and WebSocket event channels let clients watch async job progress, and the WebSocket channel endpoint multiplexes many validated request executions over a single persistent connection.
Use this page when you need to:
- Run a single HTTP request synchronously and get the response inline
- Queue an HTTP request as a background job and observe its progress
- Stream job lifecycle events over SSE or WebSocket
- Run many cURL-style requests over a single WebSocket connection
Execute HTTP requests
Section titled “Execute HTTP requests”POST /api/v1/curl/request
Section titled “POST /api/v1/curl/request”Execute an HTTP request using libcurl with full configuration. The request body is a JSON object whose declared fields cover authentication, headers, cookies, retries, and response storage. Use this endpoint for advanced scenarios: API integrations with automatic retry, large file downloads with progress tracking, and multi-step authentication flows with session cookies. This endpoint runs the request once; recurring requests are created with POST /api/v1/curl/schedule (createSchedule).
Execution modes
sync(default): blocks until completion and returns the response inlineasync: returns a job id immediately and runs the request in the background
Response modes
transparent(default): returns the raw response with the original headersjson: wraps the response in JSON with timing metrics and metadata
This endpoint takes no parameters.
Request body
Section titled “Request body”url is required; all other fields are optional. A request with data, json or form and no method is sent upstream as POST; with none of them it is sent as GET; an explicit method is always used as given, including GET with a body.
| Name | Type | Required | Description |
|---|---|---|---|
url | string | Yes | Target URL |
method | string | No | HTTP method. When absent, POST if data, json or form is set, otherwise GET. An explicit value is always used as given, including GET with a body. |
mode | string | No | Execution mode: sync or async |
response | string | No | Response mode: transparent or json |
auth_method | string | No | HTTP authentication method |
auth_user | string | No | HTTP authentication user |
auth_password | string | No | HTTP authentication password |
bearer_token | string | No | Bearer token. Sent as Authorization: Bearer <token>. An Authorization entry in headers wins: the token is then not sent, so the request carries one Authorization header. |
compressed | boolean | No | Request a compressed response |
connect_timeout | integer | No | Connect-phase timeout in seconds |
timeout | integer | No | Total request timeout in seconds |
cookie | string | No | Cookie header value |
data | string | No | Raw request body, sent as given (curl’s -d). With no method, the request is sent as POST; an explicit method is always used as given, including GET with a body. |
json | any JSON value | No | JSON request body sent with Content-Type: application/json. With no method, the request is sent as POST; an explicit method is always used as given, including GET with a body. |
form | object | No | Form fields as name -> value strings, sent URL-encoded (application/x-www-form-urlencoded). With no method, the request is sent as POST. |
headers | object | No | Extra request headers as name -> value strings |
follow_redirects | boolean | No | Follow HTTP redirects |
insecure | boolean | No | Allow insecure TLS (skip verification) |
job_name | string | No | Job name for async jobs |
keepalive | boolean | No | Enable TCP keepalive |
keepalive_time | integer | No | TCP keepalive time in seconds |
max_filesize | integer | No | Maximum response size in bytes |
max_redirects | integer | No | Maximum number of redirects to follow |
range | string | No | Byte range to request |
referer | string | No | Referer header |
retry_count | integer | No | Number of retry attempts on failure |
retry_delay | integer | No | Delay between retries in seconds |
save | boolean | No | Save the response to storage |
save_path | string | No | Custom save path, relative to downloads/by-job/{job_id} (no absolute paths or ..) |
session_id | string | No | Session id for cookie persistence |
speed_limit | integer | No | Minimum transfer rate in bytes per second |
speed_time | integer | No | Window in seconds for speed_limit |
tcp_nodelay | boolean | No | Disable Nagle’s algorithm |
user_agent | string | No | User-Agent header |
Request examples
Section titled “Request examples”curl -X POST "https://{projectId}-{containerId}-curl-1.{server}.containers.hoody.com/api/v1/curl/request" \ -H "Content-Type: application/json" \ -d '{ "url": "https://jsonplaceholder.typicode.com/posts/1", "method": "GET", "response": "json", "mode": "sync", "timeout": 30 }'import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-curl-1.{server}.containers.hoody.com', token: process.env.HOODY_TOKEN });
await client.curl.run({ url: 'https://jsonplaceholder.typicode.com/posts/1', method: 'GET', response: 'json', mode: 'sync', timeout: 30});Response fields (json mode)
Section titled “Response fields (json mode)”When response: "json" is set, the response is a JSON document with the following fields:
| Name | Type | Required | Description |
|---|---|---|---|
success | boolean | Yes | Whether the request completed without an internal error |
status_code | integer | Yes | HTTP status code returned by the upstream |
headers | object | Yes | Response headers as name -> value strings |
body | string | Yes | UTF-8 text for text responses, or base64 for binary responses. Check is_binary before decoding. |
is_binary | boolean | Yes | true if body is base64, false for UTF-8 text |
timing | object | Yes | Timing breakdown in seconds: namelookup, connect, pretransfer, redirect, starttransfer, total |
metadata | object | Yes | Response metadata: effective_url, redirect_count, size_download, size_upload, speed_download, speed_upload (all required); content_type (optional) |
job_id | string | No | Set when the request was queued as an async job |
Responses
Section titled “Responses”{ "body": "{\"userId\": 1, \"id\": 1, \"title\": \"Sample\"}", "headers": { "content-length": "245", "content-type": "application/json" }, "is_binary": false, "metadata": { "content_type": "application/json; charset=utf-8", "effective_url": "https://jsonplaceholder.typicode.com/posts/1", "redirect_count": 0, "size_download": 245, "size_upload": 0, "speed_download": 468.67, "speed_upload": 0 }, "status_code": 200, "success": true, "timing": { "connect": 0.045, "namelookup": 0.012, "pretransfer": 0.089, "redirect": 0, "starttransfer": 0.234, "total": 0.523 }}The request was accepted for async execution; a job was created.
{ "code": "INVALID_PARAMETER", "error": "Invalid request: timeout must be <= 7200"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
INVALID_PARAMETER | Invalid request | A parameter, query value or JSON body is missing, malformed or out of range | Check the message for the offending field and fix the request; retrying unchanged fails again |
INVALID_URL | Invalid or refused URL | The target URL is malformed, uses an unsupported scheme, or is refused by the outbound URL policy (for example a private address) | Provide a complete public http(s) URL |
{ "code": "ORIGIN_NOT_ALLOWED", "error": "Origin not allowed"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
ORIGIN_NOT_ALLOWED | Origin rejected | The server runs in origin-list CORS mode and the request’s Origin header is not on the list; the request is refused before it reaches a handler | Call from an allowed origin, or add the origin to the server’s --cors list |
{ "code": "SESSION_NOT_FOUND", "error": "Session not found: cart-42 (tombstoned)"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
SESSION_NOT_FOUND | Session does not exist | No session has this id, or it was deleted (a deleted id stays refused for 24 hours) | List sessions with client.curl.sessions.list(), or use a new session id |
{ "code": "PAYLOAD_TOO_LARGE", "error": "Request body too large: JSON payload (20000000 bytes) is larger than allowed (limit: 16777216 bytes)."}| Error Code | Title | Description | Resolution |
|---|---|---|---|
PAYLOAD_TOO_LARGE | Request body too large | The request body exceeds the server’s inbound limit (--max-request-body-bytes) | Send a smaller body, or raise the limit on the server |
{ "code": "STORAGE_ERROR", "error": "IO error: No space left on device (os error 28)"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
STORAGE_ERROR | Storage failure | Reading or writing the service’s storage directory failed | Retry; check the storage directory’s permissions and free space if it persists |
INTERNAL_ERROR | Service-side failure | The service itself failed (a corrupt record, a worker join, shutdown in progress) | Retry; check the server logs if it persists |
{ "code": "UPSTREAM_ERROR", "error": "cURL error: Couldn't connect to server"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
UPSTREAM_ERROR | Upstream request failed | libcurl could not complete the transfer (connect refused, TLS handshake, connection reset) | Check that the target is reachable and its TLS certificate is valid |
DNS_RESOLUTION_FAILED | Hostname did not resolve | Name resolution for the target host failed (NXDOMAIN, resolver error, empty answer) | Verify the hostname; syntactically invalid or policy-blocked URLs return 400 instead |
RESPONSE_TOO_LARGE | Upstream response too large | The upstream response grew past the request’s max_filesize (or the server’s default response cap) and the transfer was stopped | Raise max_filesize, fetch a byte range, or use mode=async with save for large downloads |
{ "code": "QUEUE_FULL", "error": "Job queue full — too many pending jobs, try again later"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
QUEUE_FULL | Job queue full | A mode=async submission was refused because the backlog is at capacity | Retry after pending jobs drain |
SSE_CAPACITY_EXHAUSTED | SSE concurrency cap reached | No slot was free under the global SSE concurrency cap; the response carries Retry-After: 5 | Honour Retry-After and retry, or reduce the number of open event streams |
{ "code": "UPSTREAM_TIMEOUT", "error": "cURL error: Timeout was reached"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
UPSTREAM_TIMEOUT | Upstream transfer timed out | The transfer or header-wait bound expired before the upstream finished (libcurl code 28) | Raise timeout, or use mode=async so the transfer outlives the HTTP request |
All error codes
Section titled “All error codes”The code field in error responses is a closed enum. Any code not in the following list does not come from this operation.
| Code |
|---|
INVALID_PARAMETER |
INVALID_URL |
INVALID_CRON_EXPRESSION |
PAYLOAD_TOO_LARGE |
ORIGIN_NOT_ALLOWED |
JOB_NOT_FOUND |
JOB_RESULT_NOT_READY |
SESSION_NOT_FOUND |
FILE_NOT_FOUND |
SCHEDULE_NOT_FOUND |
SCHEDULE_EXISTS |
REQUEST_TIMEOUT |
JOB_NOT_FINISHED |
JOB_CANCELLED |
STORAGE_ERROR |
INTERNAL_ERROR |
SSE_PARSE_ERROR |
UPSTREAM_ERROR |
DNS_RESOLUTION_FAILED |
RESPONSE_TOO_LARGE |
QUEUE_FULL |
SSE_CAPACITY_EXHAUSTED |
UPSTREAM_TIMEOUT |
NOT_FOUND |
METHOD_NOT_ALLOWED |
REQUEST_HEADERS_TOO_LARGE |
GET /api/v1/curl/request
Section titled “GET /api/v1/curl/request”Simplified interface for executing HTTP requests using URL query parameters. Best suited for simple GET requests and quick testing. For advanced features use the POST endpoint above.
Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
url | query | string | Yes | Target URL |
method | query | string | No | HTTP method (default: GET) |
response | query | string | No | Response mode: transparent or json (default: json) |
mode | query | string | No | Execution mode: sync or async (default: sync) |
session_id | query | string | No | Session ID for cookie persistence |
follow_redirects | query | boolean | No | Follow redirects (default: true) |
timeout | query | integer | No | Timeout in seconds |
user_agent | query | string | No | User-Agent header |
referer | query | string | No | Referer header |
bearer_token | query | string | No | Bearer token |
save | query | boolean | No | Save to storage |
save_path | query | string | No | Custom save path, relative to downloads/by-job/{job_id} (no absolute paths or ..) |
insecure | query | boolean | No | Allow insecure SSL |
compressed | query | boolean | No | Request compressed |
job_name | query | string | No | Job name for async |
data | query | string | No | Raw request body; alias body; presence upgrades the default method to POST |
body | query | string | No | Alias of data; data wins when both are sent |
json | query | string | No | JSON request body, sent with Content-Type: application/json; upgrades the default method to POST |
header | query | string | No | Custom header as Name: Value. Repeatable: supply once per header |
data_base64 | query | string | No | Base64 request body (binary-safe; standard or URL-safe); alias body_base64. Takes precedence over data/json; upgrades the default method to POST |
body_base64 | query | string | No | Alias of data_base64; data_base64 wins when both are sent |
Request examples
Section titled “Request examples”curl "https://{projectId}-{containerId}-curl-1.{server}.containers.hoody.com/api/v1/curl/request?url=https%3A%2F%2Fjsonplaceholder.typicode.com%2Fposts%2F1&method=GET&response=json&mode=sync&timeout=30"Response fields (json mode)
Section titled “Response fields (json mode)”When response=json is set, the response is a JSON document with the following fields:
| Name | Type | Required | Description |
|---|---|---|---|
success | boolean | Yes | Whether the request completed without an internal error |
status_code | integer | Yes | HTTP status code returned by the upstream |
headers | object | Yes | Response headers as name -> value strings |
body | string | Yes | UTF-8 text for text responses, or base64 for binary responses. Check is_binary before decoding. |
is_binary | boolean | Yes | true if body is base64, false for UTF-8 text |
timing | object | Yes | Timing breakdown in seconds: namelookup, connect, pretransfer, redirect, starttransfer, total |
metadata | object | Yes | Response metadata: effective_url, redirect_count, size_download, size_upload, speed_download, speed_upload (all required); content_type (optional) |
job_id | string | No | Set when the request was queued as an async job |
Responses
Section titled “Responses”{ "status_code": 200, "success": true}An async job was created.
{ "code": "INVALID_PARAMETER", "error": "Invalid request: timeout must be <= 7200"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
INVALID_PARAMETER | Invalid request | A parameter, query value or JSON body is missing, malformed or out of range | Check the message for the offending field and fix the request; retrying unchanged fails again |
INVALID_URL | Invalid or refused URL | The target URL is malformed, uses an unsupported scheme, or is refused by the outbound URL policy (for example a private address) | Provide a complete public http(s) URL |
{ "code": "ORIGIN_NOT_ALLOWED", "error": "Origin not allowed"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
ORIGIN_NOT_ALLOWED | Origin rejected | The server runs in origin-list CORS mode and the request’s Origin header is not on the list; the request is refused before it reaches a handler | Call from an allowed origin, or add the origin to the server’s --cors list |
{ "code": "SESSION_NOT_FOUND", "error": "Session not found: cart-42 (tombstoned)"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
SESSION_NOT_FOUND | Session does not exist | No session has this id, or it was deleted (a deleted id stays refused for 24 hours) | List sessions with client.curl.sessions.list(), or use a new session id |
{ "code": "STORAGE_ERROR", "error": "IO error: No space left on device (os error 28)"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
STORAGE_ERROR | Storage failure | Reading or writing the service’s storage directory failed | Retry; check the storage directory’s permissions and free space if it persists |
INTERNAL_ERROR | Service-side failure | The service itself failed (a corrupt record, a worker join, shutdown in progress) | Retry; check the server logs if it persists |
{ "code": "UPSTREAM_ERROR", "error": "cURL error: Couldn't connect to server"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
UPSTREAM_ERROR | Upstream request failed | libcurl could not complete the transfer (connect refused, TLS handshake, connection reset) | Check that the target is reachable and its TLS certificate is valid |
DNS_RESOLUTION_FAILED | Hostname did not resolve | Name resolution for the target host failed (NXDOMAIN, resolver error, empty answer) | Verify the hostname; syntactically invalid or policy-blocked URLs return 400 instead |
RESPONSE_TOO_LARGE | Upstream response too large | The upstream response grew past the request’s max_filesize (or the server’s default response cap) and the transfer was stopped | Raise max_filesize, fetch a byte range, or use mode=async with save for large downloads |
{ "code": "QUEUE_FULL", "error": "Job queue full — too many pending jobs, try again later"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
QUEUE_FULL | Job queue full | A mode=async submission was refused because the backlog is at capacity | Retry after pending jobs drain |
SSE_CAPACITY_EXHAUSTED | SSE concurrency cap reached | No slot was free under the global SSE concurrency cap; the response carries Retry-After: 5 | Honour Retry-After and retry, or reduce the number of open event streams |
{ "code": "UPSTREAM_TIMEOUT", "error": "cURL error: Timeout was reached"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
UPSTREAM_TIMEOUT | Upstream transfer timed out | The transfer or header-wait bound expired before the upstream finished (libcurl code 28) | Raise timeout, or use mode=async so the transfer outlives the HTTP request |
All error codes
Section titled “All error codes”The code field in error responses is a closed enum. Any code not in the following list does not come from this operation.
| Code |
|---|
INVALID_PARAMETER |
INVALID_URL |
INVALID_CRON_EXPRESSION |
PAYLOAD_TOO_LARGE |
ORIGIN_NOT_ALLOWED |
JOB_NOT_FOUND |
JOB_RESULT_NOT_READY |
SESSION_NOT_FOUND |
FILE_NOT_FOUND |
SCHEDULE_NOT_FOUND |
SCHEDULE_EXISTS |
REQUEST_TIMEOUT |
JOB_NOT_FINISHED |
JOB_CANCELLED |
STORAGE_ERROR |
INTERNAL_ERROR |
SSE_PARSE_ERROR |
UPSTREAM_ERROR |
DNS_RESOLUTION_FAILED |
RESPONSE_TOO_LARGE |
QUEUE_FULL |
SSE_CAPACITY_EXHAUSTED |
UPSTREAM_TIMEOUT |
NOT_FOUND |
METHOD_NOT_ALLOWED |
REQUEST_HEADERS_TOO_LARGE |
Subscribe to job events
Section titled “Subscribe to job events”GET /api/v1/curl/sse
Section titled “GET /api/v1/curl/sse”Establish a Server-Sent Events connection. Each job lifecycle event is delivered as a standard SSE frame with event: set to the message type and data: containing the JSON payload.
Events:
jobstarteddata is{type, job_id, name, seq}jobprogressdata is{type, job_id, progress}jobcompleteddata is{type, job_id, status, seq}errordata is{type, message}laggeddata is{missed, code, min_seq, max_seq, incarnation}: lifecycle events this client should have had are gone (see below)
Filtering: pass ?job_id= to receive events for a single job only.
Resuming after a disconnect: jobstarted and jobcompleted frames carry their seq as the SSE id: (other events have no id:), so an EventSource reconnect sends it back as Last-Event-ID; ?since= does the same and wins over the header (since=0: everything the server still keeps). The lifecycle events after it are sent first, then live events; progress events are not replayed. The server keeps the last 4096 lifecycle events. When the ones after the cursor are no longer all kept, or the server restarted, a lagged event with code: "replay_gap" comes first (missed is then a lower bound, 1 when the number is unknown), then everything still kept: re-read the jobs you wait on with client.curl.jobs.get(id). A live connection that falls behind gets the missed lifecycle events from the same store, or a lagged event with code: "lagged" if they are gone. A lagged event carries as id: the seq just before the first event still kept (max_seq when none is), so the next reconnect resumes after what is lost. On Last-Event-ID, 0 is read as an id from an older server that numbered frames per connection, so it gets the notice; use since=0 for everything kept. Without a cursor the stream is live only.
Capacity: the connection counts against the global SSE concurrency cap. When exhausted the server returns 503.
Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
job_id | query | string | No | Optional job ID filter |
since | query | integer | No | Resume cursor: the last seq (SSE id:) received; 0 for everything still kept. Wins over Last-Event-ID |
incarnation | query | string | No | The incarnation of the last lagged event received; a different one means the server restarted since |
Last-Event-ID | header | string | No | Resume cursor as sent by an EventSource reconnect: the last id: received. Ignored when it is not a number or when since is given |
Request examples
Section titled “Request examples”curl -N "https://{projectId}-{containerId}-curl-1.{server}.containers.hoody.com/api/v1/curl/sse?job_id=3f2b8c1e-9d4a-4c6b-8e21-5a7f0c9d1b42"import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-curl-1.{server}.containers.hoody.com', token: process.env.HOODY_TOKEN });
const stream = await client.curl.jobs.stream({ job_id: '3f2b8c1e-9d4a-4c6b-8e21-5a7f0c9d1b42' });for await (const event of stream) { // event.event, event.data}Responses
Section titled “Responses”retry: 5000
event: jobstartedid: 1data: {"type":"jobstarted","job_id":"3f2b8c1e-9d4a-4c6b-8e21-5a7f0c9d1b42","name":"fetch-feed","seq":1}
event: jobprogressdata: {"type":"jobprogress","job_id":"3f2b8c1e-9d4a-4c6b-8e21-5a7f0c9d1b42","progress":0.42}
event: jobcompletedid: 2data: {"type":"jobcompleted","job_id":"3f2b8c1e-9d4a-4c6b-8e21-5a7f0c9d1b42","status":"completed","seq":2}The stream is text/event-stream. The first frame is retry: 5000. Each event frame carries event: set to the message type and data: containing the JSON payload whose fields are named inline above; jobstarted, jobcompleted and lagged frames also carry id:, the lifecycle seq to resume from (a lagged frame’s id: is the seq just before the first event still kept). lagged is {missed, code, min_seq, max_seq, incarnation} with no type. Idle periods get : comment heartbeats. The stream does not end on its own.
{ "code": "INVALID_PARAMETER", "error": "Invalid request: timeout must be <= 7200"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
INVALID_PARAMETER | Invalid request | A parameter, query value or JSON body is missing, malformed or out of range | Check the message for the offending field and fix the request; retrying unchanged fails again |
{ "code": "ORIGIN_NOT_ALLOWED", "error": "Origin not allowed"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
ORIGIN_NOT_ALLOWED | Origin rejected | The server runs in origin-list CORS mode and the request’s Origin header is not on the list; the request is refused before it reaches a handler | Call from an allowed origin, or add the origin to the server’s --cors list |
{ "code": "SSE_CAPACITY_EXHAUSTED", "error": "Server-Sent Events concurrency cap exhausted"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
SSE_CAPACITY_EXHAUSTED | SSE concurrency cap reached | No slot was free under the global SSE concurrency cap; the response carries Retry-After: 5 | Honour Retry-After and retry, or reduce the number of open event streams |
All error codes
Section titled “All error codes”The code field in error responses is a closed enum. Any code not in the following list does not come from this operation.
| Code |
|---|
INVALID_PARAMETER |
INVALID_URL |
INVALID_CRON_EXPRESSION |
PAYLOAD_TOO_LARGE |
ORIGIN_NOT_ALLOWED |
JOB_NOT_FOUND |
JOB_RESULT_NOT_READY |
SESSION_NOT_FOUND |
FILE_NOT_FOUND |
SCHEDULE_NOT_FOUND |
SCHEDULE_EXISTS |
REQUEST_TIMEOUT |
JOB_NOT_FINISHED |
JOB_CANCELLED |
STORAGE_ERROR |
INTERNAL_ERROR |
SSE_PARSE_ERROR |
UPSTREAM_ERROR |
DNS_RESOLUTION_FAILED |
RESPONSE_TOO_LARGE |
QUEUE_FULL |
SSE_CAPACITY_EXHAUSTED |
UPSTREAM_TIMEOUT |
NOT_FOUND |
METHOD_NOT_ALLOWED |
REQUEST_HEADERS_TOO_LARGE |
GET /api/v1/curl/ws
Section titled “GET /api/v1/curl/ws”Establish a WebSocket connection that receives job lifecycle events as JSON text frames.
Messages:
jobstartedpayload is{job_id, name, seq}jobprogresspayload is{job_id, progress}(progress is a 0..=1 fraction)jobcompletedpayload is{job_id, status, seq};seqgrows by one per lifecycle event (progress events have none)errorpayload is{message}
Filtering: pass ?job_id= to only receive events for a single job.
Resuming after a disconnect: jobstarted and jobcompleted carry seq, a number that grows by one per lifecycle event (progress events have none). Connect with since = the last seq you received (since=0 on a first connection: everything the server still keeps) and incarnation: the lifecycle events after it are sent first, then {"type":"replay_boundary","max_seq","incarnation"}, then live events. Keep max_seq as the cursor until a numbered event arrives. Without since the stream is live only. The server keeps the last 4096 lifecycle events. When the ones after your cursor are no longer all kept, or the server restarted, a {"type":"lagged","code":"replay_gap","min_seq","max_seq","incarnation"} frame comes first, then everything still kept: re-read the jobs you wait on with client.curl.jobs.get(id). A live connection that falls behind gets the missed lifecycle events from the same store, or a lagged frame with code: "lagged" if they are gone.
Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
job_id | query | string | No | Optional job ID filter |
since | query | integer | No | Resume cursor: the last seq (or replay_boundary.max_seq) received; 0 for everything still kept. The lifecycle events after it are replayed first, then a replay_boundary frame |
incarnation | query | string | No | The incarnation of the last control frame received; a different one means the server restarted since |
Request examples
Section titled “Request examples”curl --include \ -H "Connection: Upgrade" \ -H "Upgrade: websocket" \ -H "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==" \ -H "Sec-WebSocket-Version: 13" \ "https://{projectId}-{containerId}-curl-1.{server}.containers.hoody.com/api/v1/curl/ws?job_id=3f2b8c1e-9d4a-4c6b-8e21-5a7f0c9d1b42"import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-curl-1.{server}.containers.hoody.com', token: process.env.HOODY_TOKEN });
const socket = await client.curl.jobs.connect({ job_id: '3f2b8c1e-9d4a-4c6b-8e21-5a7f0c9d1b42' });socket.onJobstarted((msg) => console.log('started', msg.job_id, msg.name));socket.onJobprogress((msg) => console.log('progress', msg.job_id, msg.progress));socket.onJobcompleted((msg) => console.log('completed', msg.job_id, msg.status));socket.onError((err) => console.error(err));await socket.connect();// later: socket.close();Responses
Section titled “Responses”The server returns 101 Switching Protocols and the connection upgrades to a WebSocket. A 101 response carries no body.
{ "code": "INVALID_PARAMETER", "error": "Invalid request: timeout must be <= 7200"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
INVALID_PARAMETER | Invalid request | A parameter, query value or JSON body is missing, malformed or out of range | Check the message for the offending field and fix the request; retrying unchanged fails again |
{ "code": "ORIGIN_NOT_ALLOWED", "error": "Origin not allowed"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
ORIGIN_NOT_ALLOWED | Origin rejected | The server runs in origin-list CORS mode and the request’s Origin header is not on the list; the request is refused before it reaches a handler | Call from an allowed origin, or add the origin to the server’s --cors list |
All error codes
Section titled “All error codes”The code field in error responses is a closed enum. Any code not in the following list does not come from this operation.
| Code |
|---|
INVALID_PARAMETER |
INVALID_URL |
INVALID_CRON_EXPRESSION |
PAYLOAD_TOO_LARGE |
ORIGIN_NOT_ALLOWED |
JOB_NOT_FOUND |
JOB_RESULT_NOT_READY |
SESSION_NOT_FOUND |
FILE_NOT_FOUND |
SCHEDULE_NOT_FOUND |
SCHEDULE_EXISTS |
REQUEST_TIMEOUT |
JOB_NOT_FINISHED |
JOB_CANCELLED |
STORAGE_ERROR |
INTERNAL_ERROR |
SSE_PARSE_ERROR |
UPSTREAM_ERROR |
DNS_RESOLUTION_FAILED |
RESPONSE_TOO_LARGE |
QUEUE_FULL |
SSE_CAPACITY_EXHAUSTED |
UPSTREAM_TIMEOUT |
NOT_FOUND |
METHOD_NOT_ALLOWED |
REQUEST_HEADERS_TOO_LARGE |
Multiplexed WebSocket channel
Section titled “Multiplexed WebSocket channel”GET /api/v1/curl/channel
Section titled “GET /api/v1/curl/channel”Establish a persistent WebSocket connection for multiplexed, validated CurlRequest execution. This endpoint is separate from /api/v1/curl/ws, which only streams async job lifecycle events.
The channel is buffered: each upstream response is still fully buffered by hoody-curl before response body chunks are emitted over WebSocket. Query parameters configure per-connection limits such as max_concurrent, max_queue, max_frame_bytes, max_request_bytes, chunk_bytes, stream_timeout_secs, and idle_timeout_secs.
Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
max_concurrent | query | integer | No | Alias for max concurrent streams on this channel connection |
max_concurrent_streams | query | integer | No | Maximum concurrently executing streams on this channel connection |
max_pool | query | integer | No | Alias for max_concurrent; does not configure outbound libcurl connection pooling |
max_queue | query | integer | No | Maximum queued streams waiting for a per-connection execution slot |
max_frame_bytes | query | integer | No | Maximum inbound WebSocket text frame size in bytes |
max_request_bytes | query | integer | No | Maximum assembled request JSON size in bytes |
chunk_bytes | query | integer | No | Maximum upstream response bytes encoded into one channel body frame |
stream_timeout_secs | query | integer | No | Per-stream execution timeout in seconds |
idle_timeout_secs | query | integer | No | Idle channel timeout in seconds |
max_outbound_messages | query | integer | No | Maximum queued outbound channel messages |
binary | query | boolean | No | true negotiates binary frames: response bodies arrive as binary BODY frames and request.start may set binary_body. Default false |
Request examples
Section titled “Request examples”curl --include \ -H "Connection: Upgrade" \ -H "Upgrade: websocket" \ -H "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==" \ -H "Sec-WebSocket-Version: 13" \ "https://{projectId}-{containerId}-curl-1.{server}.containers.hoody.com/api/v1/curl/channel?max_concurrent=4&chunk_bytes=8192&stream_timeout_secs=60"import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-curl-1.{server}.containers.hoody.com', token: process.env.HOODY_TOKEN });
const channel = await client.curl.channel.connect({ limits: { maxConcurrentStreams: 4, chunkBytes: 8192, streamTimeoutSecs: 60 } });Responses
Section titled “Responses”The server returns 101 Switching Protocols and the connection upgrades to a WebSocket. A 101 response carries no body.
{ "code": "INVALID_PARAMETER", "error": "Invalid request: timeout must be <= 7200"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
INVALID_PARAMETER | Invalid request | A parameter, query value or JSON body is missing, malformed or out of range | Check the message for the offending field and fix the request; retrying unchanged fails again |
{ "code": "ORIGIN_NOT_ALLOWED", "error": "Origin not allowed"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
ORIGIN_NOT_ALLOWED | Origin rejected | The server runs in origin-list CORS mode and the request’s Origin header is not on the list; the request is refused before it reaches a handler | Call from an allowed origin, or add the origin to the server’s --cors list |
All error codes
Section titled “All error codes”The code field in error responses is a closed enum. Any code not in the following list does not come from this operation.
| Code |
|---|
INVALID_PARAMETER |
INVALID_URL |
INVALID_CRON_EXPRESSION |
PAYLOAD_TOO_LARGE |
ORIGIN_NOT_ALLOWED |
JOB_NOT_FOUND |
JOB_RESULT_NOT_READY |
SESSION_NOT_FOUND |
FILE_NOT_FOUND |
SCHEDULE_NOT_FOUND |
SCHEDULE_EXISTS |
REQUEST_TIMEOUT |
JOB_NOT_FINISHED |
JOB_CANCELLED |
STORAGE_ERROR |
INTERNAL_ERROR |
SSE_PARSE_ERROR |
UPSTREAM_ERROR |
DNS_RESOLUTION_FAILED |
RESPONSE_TOO_LARGE |
QUEUE_FULL |
SSE_CAPACITY_EXHAUSTED |
UPSTREAM_TIMEOUT |
NOT_FOUND |
METHOD_NOT_ALLOWED |
REQUEST_HEADERS_TOO_LARGE |