Docs
Jobs and batches
Run many requests, or one long list walk, in the background: same prices and caches as single requests, with results you can stream, download or receive by webhook.
A job runs up to 1,000 endpoint requests for you in the background. Use one when a task needs many items ("these 300 videos"), a whole list ("every comment"), or a request that can take a while, such as a long transcript. Each item is exactly the request its endpoint answers synchronously: same parameters, same price, same caches, and its result is the same envelope, so the code that reads a single response reads a job's results too. Failed items cost nothing.
| Route | What it does | Key scope |
|---|---|---|
POST /v1/jobs | Run many requests (or one long one) as a job | jobs |
GET /v1/jobs | List jobs, newest first | jobs |
GET /v1/jobs/{id} | Get a job (free to poll) | jobs |
GET /v1/jobs/{id}/results | List a job's results as they complete | jobs |
GET /v1/jobs/{id}/results.jsonl | Download every result of a job (JSON Lines) | none (signed URL) |
POST /v1/jobs/{id}/cancel | Cancel a job | jobs |
Create a job
Send the requests with POST /v1/jobs. Each one names an endpoint by ID (the x-socialtap-endpoint of its
operation in the API reference) and its parameters, as you would send them to the endpoint, plus
your own custom_id:
curl -X POST 'https://staging.socialtap.dev/v1/jobs?wait=60' \
-H "x-api-key: $SOCIALTAP_API_KEY" \
-H "idempotency-key: $(uuidgen)" \
-H 'content-type: application/json' \
--data '{
"requests": [
{
"custom_id": "row-1",
"endpoint": "youtube.transcript",
"params": {
"url": "dQw4w9WgXcQ"
}
}
]
}'const res = await fetch("https://staging.socialtap.dev/v1/jobs?wait=60", {
method: "POST",
headers: { "x-api-key": process.env.SOCIALTAP_API_KEY ?? "", "idempotency-key": crypto.randomUUID(), "content-type": "application/json" },
body: JSON.stringify({
"requests": [
{
"custom_id": "row-1",
"endpoint": "youtube.transcript",
"params": {
"url": "dQw4w9WgXcQ"
}
}
]
}),
});
const body = await res.json();
// 200: body.results holds the first page of results; 202: still running, poll body.data.id.
console.log(res.status, body.data.status, body.data.request_counts);| Field | Type | Description |
|---|---|---|
requests | object[] | The requests to run, 1 to max_requests; custom_id must be unique within the job. |
requests[].custom_id | string | Your ID for the item (letters, digits, _ and -), echoed on its results. |
requests[].endpoint | string | The endpoint ID (youtube.transcript). |
requests[].params | object | The endpoint's query params, as you would send them to it (strings, numbers or booleans). |
requests[].max_pages? | integer or null | List endpoints only: walk up to this many pages with the cursor (default 1). Each page is a result and costs a page's price. |
max_age? | integer or null | Accept cached results up to this many seconds old for every item (free cache hits), like the endpoints' max_age. |
max_credits? | integer or null | Most the job may spend; reserved at creation. Default: the sum of every item's maximum price (times its max_pages). Items that would go over it are canceled. |
webhook_url? | string or null | Also deliver this job's job.* events to this URL: the URL of one of the account's webhook endpoints (its secret signs them). |
metadata? | object | Up to metadata_max_keys string pairs of your own, returned with the job and in its events. |
Every item is checked before anything runs: an unknown endpoint or parameter, a missing required parameter, a
duplicate custom_id or a list option on a single-item endpoint fails the whole request with
invalid_input, and its param names the field, such as requests[3].params.url. Nothing is charged.
Synchronous batches. With wait (up to 1 minute), the request waits for the job to end. If it
ends in time, the answer is 200 with the job and the first page of its results in results (continue from
results_page.next_cursor). Otherwise it is 202 with the job as it is, and its URL in the
location header.
Any endpoint, in the background. A single endpoint request with the header Prefer: respond-async becomes a
one-request job: the answer is 202 with the job instead of the result. Its one item's custom_id is
request.
Walk whole lists
On a list endpoint, max_pages (up to 1,000 per item) walks the list for you with its
cursor: every page is one result with its page_index, priced as one page. The walk stops at the end of the list, at
max_pages, or when the job's credits run out. A job holds at most 10,000 pages in total.
The job
Poll GET /v1/jobs/{id}: it is free, and wait long-polls until the job ends. Or subscribe a
webhook to job.completed.
| Field | Type | Description |
|---|---|---|
object | string | Always job. One of job |
id | string | The job ID (job_…). |
status | string | queued: nothing started yet. running. canceling: canceled, running items finishing. completed: every item ended (check request_counts: there is no job-level failure). canceled. expired: the deadline passed; unfinished items expired. One of queued, running, canceling, completed, canceled, expired |
request_counts | object | |
request_counts.total | integer | |
request_counts.queued | integer | Not started. |
request_counts.running | integer | Started (fetching, waiting for a retry or walking pages). |
request_counts.succeeded | integer | |
request_counts.failed | integer | |
request_counts.canceled | integer | |
request_counts.expired | integer | |
credits | object | |
credits.max | integer | max_credits: the most the job may spend. |
credits.reserved | integer | Credits still held for items not ended (0 once the job ends). |
credits.charged | integer | Credits charged so far (items' prices; failed items are free). |
max_age | integer or null | The job's max_age; null when it accepts no cached results. |
metadata | object | Your metadata; {} when none. |
webhook_url | string or null | The job's own webhook URL; null when none. |
created_at | string (date-time) | |
started_at | string (date-time) or null | When the first item started; null before. |
finished_at | string (date-time) or null | When the job ended; null while it runs. |
cancel_requested_at | string (date-time) or null | |
expires_at | string (date-time) | The deadline: items not ended by then expire. |
results_url | string or null | Every result as JSON Lines (one JobResult per line), signed: works without the API key until results_url_expires_at. Null until the job ends and after results_expire_at. |
results_url_expires_at | string (date-time) or null | |
results_expire_at | string (date-time) or null | When results are deleted; null while the job runs. |
A job is queued until its first item starts, running while items run, and completed once every item has ended:
there is no failed job, items fail one by one, so check request_counts. A canceled job is canceling until its
running items finish, then canceled. A job that reaches expires_at (its deadline, 1 day after
creation) ends expired: items that had not started expire, running ones finish.
Results
GET /v1/jobs/{id}/results lists results in the order they completed, cursor-paged, while the job runs and after. Keep the
last page.next_cursor to read only what arrived since. Each result is one page of one item:
| Field | Type | Description |
|---|---|---|
object | string | Always job_result. One of job_result |
custom_id | string | |
endpoint | string | |
page_index | integer | 0 for a single-item endpoint and a list's first page; n for the list's page n+1. |
status | string | The page's outcome; an item's status is its last result's. canceled and expired results carry no result (error says why when it is not the job's cancel or deadline). One of succeeded, failed, canceled, expired |
status_code | integer or null | The HTTP status the synchronous request answered; null when it did not run. |
result | object or null | Exactly the synchronous success envelope; null unless succeeded. |
result.data | any | The endpoint's data: its resource, or a list page's items. |
result.page? | object or null | List endpoints only. |
result.page.has_more | boolean | True when another page exists; then next_cursor is set. An empty page with has_more true is not the end. |
result.page.next_cursor | string or null | Pass as cursor, with the same other parameters, to get the next page; null at the end. |
result.page.next_cursor_expires_at | string (date-time) or null | When next_cursor stops working, RFC 3339 UTC; null at the end. Don't store cursors. |
result.page.end_reason | string or null | Why there are no more pages; null while has_more is true. end: the list ended, or reached since/until. platform_limit: the platform stops serving further pages although the list goes on. max_pages: SocialTap's cap on pages in one walk. New values may be added. A short or empty page is not the end: only has_more false is. One of end, platform_limit, max_pages |
result.meta | object | |
result.meta.request_id | string | Unique request ID; include it when contacting support. |
result.meta.platform | string | Platform ID of the endpoint (youtube, tiktok, …). |
result.meta.source | string | Which upstream strategy produced the data. |
result.meta.is_cached | boolean | True when served from the cache (free); see max_age. |
result.meta.credits_charged | integer | Credits charged for this request (0 for errors and cache hits). |
result.meta.latency_ms | integer | Time SocialTap spent on the request, in milliseconds. |
result.meta.fetched_at | string (date-time) | When the data was fetched from the platform, RFC 3339 UTC (older than the request for cache hits). |
result.meta.input | object or null | What the request's url or handle resolved to: kind, platform ID and canonical URL, so you can confirm what was fetched; null for searches. |
result.meta.credits_remaining | integer or null | Account balance after this request (the x-credits-remaining header); for a busy account an estimate that may not yet subtract concurrent requests (about a second of usage), never below the real balance; null when SocialTap could not read it. |
result.meta.client_request_id | string or null | Your x-client-request-id, echoed; null when you sent none. |
result.meta.warnings | object[] | Things to act on that did not fail the request: a deprecated endpoint, partial data. [] when none. |
result.meta.mode | string or null | live, or test for a test key's answer: a recorded response (fixture), free and deterministic; null when SocialTap could not tell. One of live, test |
error | object or null | Exactly the synchronous error object; null on success and for the job's own cancel or deadline. |
error.code | string | Machine-readable error code (see the error codes list); stable. |
error.message | string | What went wrong and how to fix it, for humans and agents; wording may change. |
error.request_id | string | Unique request ID; include it when contacting support. |
error.status | integer | The HTTP status of the response, repeated for clients that only see the body. |
error.retryable | boolean | True when repeating the same request later may succeed (after details.retry_after when present). |
error.param | string or null | The query parameter or header the error is about; null when it is not about one input. |
error.remediation | string | What to do about this code, one sentence. |
error.doc_url | string | URL of this code's documentation. |
error.details | object | The code's dynamic values (the error codes list documents each code's keys); {} when none. |
error.charged | boolean | Whether the request was charged: always false, failed requests are free. |
completed_at | string (date-time) |
resultis exactly the synchronous response's envelope (data,meta, andpageon list endpoints).erroris exactly the synchronous error object: branch onerror.codeas you would for a single request.- An item that ended without a fetched page (canceled, expired) has one result with that status and no
result. ?status=failedlists the failures, to resubmit them in a new job.
Once the job ends, results_url is a signed link to every result as JSON Lines (one result per line), valid
1 hour and usable without an API key: hand it to a download tool or another service. Get
the job again for a fresh link.
Results are kept 7 days after the job ends and job records
30 days; after that the job answers job_not_found.
Credits
- Reserve. Creating a job sets
max_creditsaside: by default the most its items can cost (each item's maximum price, times itsmax_pages). Lower it to cap what the job may spend. If your balance can't cover it, the request fails withinsufficient_creditsand nothing runs; if the key has a spend limit,max_creditsmust also fit in what the limit leaves this interval, elsespend_limit_reached. - Charge. Each page is charged its own price as it ends, exactly like a single request: cache hits and failures
are free. A page that the remaining budget can't pay for does not run: its item is
canceledwith aninsufficient_creditserror whosedetails.max_creditssays the job's cap stopped it, orspend_limit_reachedwhen the key's spend limit did. Pages count against the key that created the job, exactly as its own requests do. - Release. When the job ends, whatever is still set aside goes back to your balance.
credits.chargedis what the job cost.
Keys and test mode
Creating a job needs a key with the jobs scope, and endpoints too: a job's items are endpoint requests. A test
key's jobs run every item in test mode: each result is exactly the recorded answer the endpoint
gives that test key, free, and nothing is set aside or charged. Test jobs are separate from live ones: a test key
lists and reads its own, a live key never sees them.
Cancel
POST /v1/jobs/{id}/cancel stops a job: items that haven't started are canceled (free), items already running finish and
are charged. Canceling a job that has ended changes nothing.
Limits and fairness
A job runs up to 10 items at a time, and all of an account's jobs together up to
25, so a large job never crowds out your own synchronous requests. When SocialTap is
busy, a job's items wait for capacity instead of failing; an item whose platform fails is retried up to
3 times before it fails. The unfinished jobs of an account hold at most
50,000 pages; above that, creating a job answers rate_limited: wait for running
jobs to end.
Retry safely
Send an idempotency-key with every create: if the connection drops, repeat the request with the same key
and body and you get the job the first request created, never a second one. See idempotency.