Skip to content
Hoody.com

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 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 inline
  • async: returns a job id immediately and runs the request in the background

Response modes

  • transparent (default): returns the raw response with the original headers
  • json: wraps the response in JSON with timing metrics and metadata

This endpoint takes no parameters.

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.

NameTypeRequiredDescription
urlstringYesTarget URL
methodstringNoHTTP 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.
modestringNoExecution mode: sync or async
responsestringNoResponse mode: transparent or json
auth_methodstringNoHTTP authentication method
auth_userstringNoHTTP authentication user
auth_passwordstringNoHTTP authentication password
bearer_tokenstringNoBearer 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.
compressedbooleanNoRequest a compressed response
connect_timeoutintegerNoConnect-phase timeout in seconds
timeoutintegerNoTotal request timeout in seconds
cookiestringNoCookie header value
datastringNoRaw 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.
jsonany JSON valueNoJSON 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.
formobjectNoForm fields as name -> value strings, sent URL-encoded (application/x-www-form-urlencoded). With no method, the request is sent as POST.
headersobjectNoExtra request headers as name -> value strings
follow_redirectsbooleanNoFollow HTTP redirects
insecurebooleanNoAllow insecure TLS (skip verification)
job_namestringNoJob name for async jobs
keepalivebooleanNoEnable TCP keepalive
keepalive_timeintegerNoTCP keepalive time in seconds
max_filesizeintegerNoMaximum response size in bytes
max_redirectsintegerNoMaximum number of redirects to follow
rangestringNoByte range to request
refererstringNoReferer header
retry_countintegerNoNumber of retry attempts on failure
retry_delayintegerNoDelay between retries in seconds
savebooleanNoSave the response to storage
save_pathstringNoCustom save path, relative to downloads/by-job/{job_id} (no absolute paths or ..)
session_idstringNoSession id for cookie persistence
speed_limitintegerNoMinimum transfer rate in bytes per second
speed_timeintegerNoWindow in seconds for speed_limit
tcp_nodelaybooleanNoDisable Nagle’s algorithm
user_agentstringNoUser-Agent header
Terminal window
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
}'

When response: "json" is set, the response is a JSON document with the following fields:

NameTypeRequiredDescription
successbooleanYesWhether the request completed without an internal error
status_codeintegerYesHTTP status code returned by the upstream
headersobjectYesResponse headers as name -> value strings
bodystringYesUTF-8 text for text responses, or base64 for binary responses. Check is_binary before decoding.
is_binarybooleanYestrue if body is base64, false for UTF-8 text
timingobjectYesTiming breakdown in seconds: namelookup, connect, pretransfer, redirect, starttransfer, total
metadataobjectYesResponse metadata: effective_url, redirect_count, size_download, size_upload, speed_download, speed_upload (all required); content_type (optional)
job_idstringNoSet when the request was queued as an async job
{
"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 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

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.

NameInTypeRequiredDescription
urlquerystringYesTarget URL
methodquerystringNoHTTP method (default: GET)
responsequerystringNoResponse mode: transparent or json (default: json)
modequerystringNoExecution mode: sync or async (default: sync)
session_idquerystringNoSession ID for cookie persistence
follow_redirectsquerybooleanNoFollow redirects (default: true)
timeoutqueryintegerNoTimeout in seconds
user_agentquerystringNoUser-Agent header
refererquerystringNoReferer header
bearer_tokenquerystringNoBearer token
savequerybooleanNoSave to storage
save_pathquerystringNoCustom save path, relative to downloads/by-job/{job_id} (no absolute paths or ..)
insecurequerybooleanNoAllow insecure SSL
compressedquerybooleanNoRequest compressed
job_namequerystringNoJob name for async
dataquerystringNoRaw request body; alias body; presence upgrades the default method to POST
bodyquerystringNoAlias of data; data wins when both are sent
jsonquerystringNoJSON request body, sent with Content-Type: application/json; upgrades the default method to POST
headerquerystringNoCustom header as Name: Value. Repeatable: supply once per header
data_base64querystringNoBase64 request body (binary-safe; standard or URL-safe); alias body_base64. Takes precedence over data/json; upgrades the default method to POST
body_base64querystringNoAlias of data_base64; data_base64 wins when both are sent
Terminal window
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"

When response=json is set, the response is a JSON document with the following fields:

NameTypeRequiredDescription
successbooleanYesWhether the request completed without an internal error
status_codeintegerYesHTTP status code returned by the upstream
headersobjectYesResponse headers as name -> value strings
bodystringYesUTF-8 text for text responses, or base64 for binary responses. Check is_binary before decoding.
is_binarybooleanYestrue if body is base64, false for UTF-8 text
timingobjectYesTiming breakdown in seconds: namelookup, connect, pretransfer, redirect, starttransfer, total
metadataobjectYesResponse metadata: effective_url, redirect_count, size_download, size_upload, speed_download, speed_upload (all required); content_type (optional)
job_idstringNoSet when the request was queued as an async job
{
"status_code": 200,
"success": true
}

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

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:

  • jobstarted data is {type, job_id, name, seq}
  • jobprogress data is {type, job_id, progress}
  • jobcompleted data is {type, job_id, status, seq}
  • error data is {type, message}
  • lagged data 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.

NameInTypeRequiredDescription
job_idquerystringNoOptional job ID filter
sincequeryintegerNoResume cursor: the last seq (SSE id:) received; 0 for everything still kept. Wins over Last-Event-ID
incarnationquerystringNoThe incarnation of the last lagged event received; a different one means the server restarted since
Last-Event-IDheaderstringNoResume cursor as sent by an EventSource reconnect: the last id: received. Ignored when it is not a number or when since is given
Terminal window
curl -N "https://{projectId}-{containerId}-curl-1.{server}.containers.hoody.com/api/v1/curl/sse?job_id=3f2b8c1e-9d4a-4c6b-8e21-5a7f0c9d1b42"
retry: 5000
event: jobstarted
id: 1
data: {"type":"jobstarted","job_id":"3f2b8c1e-9d4a-4c6b-8e21-5a7f0c9d1b42","name":"fetch-feed","seq":1}
event: jobprogress
data: {"type":"jobprogress","job_id":"3f2b8c1e-9d4a-4c6b-8e21-5a7f0c9d1b42","progress":0.42}
event: jobcompleted
id: 2
data: {"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.

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

Establish a WebSocket connection that receives job lifecycle events as JSON text frames.

Messages:

  • jobstarted payload is {job_id, name, seq}
  • jobprogress payload is {job_id, progress} (progress is a 0..=1 fraction)
  • jobcompleted payload is {job_id, status, seq}; seq grows by one per lifecycle event (progress events have none)
  • error payload 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.

NameInTypeRequiredDescription
job_idquerystringNoOptional job ID filter
sincequeryintegerNoResume 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
incarnationquerystringNoThe incarnation of the last control frame received; a different one means the server restarted since
Terminal window
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"

The server returns 101 Switching Protocols and the connection upgrades to a WebSocket. A 101 response carries no body.

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

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.

NameInTypeRequiredDescription
max_concurrentqueryintegerNoAlias for max concurrent streams on this channel connection
max_concurrent_streamsqueryintegerNoMaximum concurrently executing streams on this channel connection
max_poolqueryintegerNoAlias for max_concurrent; does not configure outbound libcurl connection pooling
max_queuequeryintegerNoMaximum queued streams waiting for a per-connection execution slot
max_frame_bytesqueryintegerNoMaximum inbound WebSocket text frame size in bytes
max_request_bytesqueryintegerNoMaximum assembled request JSON size in bytes
chunk_bytesqueryintegerNoMaximum upstream response bytes encoded into one channel body frame
stream_timeout_secsqueryintegerNoPer-stream execution timeout in seconds
idle_timeout_secsqueryintegerNoIdle channel timeout in seconds
max_outbound_messagesqueryintegerNoMaximum queued outbound channel messages
binaryquerybooleanNotrue negotiates binary frames: response bodies arrive as binary BODY frames and request.start may set binary_body. Default false
Terminal window
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"

The server returns 101 Switching Protocols and the connection upgrades to a WebSocket. A 101 response carries no body.

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