Jobs and webhooks
Wait for results, resolve failures, and receive signed job events.
An accepted request returns HTTP 202 and a data.jobId. Save that ID before you wait for the result. All routes below use the /api/v1 prefix.
Wait for a result
Read GET /jobs/{jobId}. The response includes status, progress, child counts, output references, and safe errors. A child is one item of work in the job. Error responses include a requestId that you can give to Sovran support.
| Status | Your next action |
|---|---|
queued | Keep reading the same job. Its inputs are saved. |
running | Keep reading the same job. Do not submit it again. |
succeeded | Use its output references. For renders, read GET /renders/{renderId}/outputs to get a temporary download URL. |
partially_succeeded | Keep the successful outputs. Read the failed child errors before you request a retry. |
failed | Read error and any child errors. Resolve the cause before you request a supported retry. |
requires_action or manual_review | Stop automatic submission. Follow Resolve a blocked job. |
canceled | Stop polling. Confirm why the work was stopped before you make a new request. |
Download the Node.js polling example. It needs Node.js 22 or later and no extra packages. Set your origin and key as shown in Authentication.
curl --fail --output poll-job.mjs "$SOVRAN_API_ORIGIN/docs/api/poll-job.mjs"
node poll-job.mjs JOB_UUIDReplace JOB_UUID with the accepted data.jobId. The example waits 1, 2, 5, then 10 seconds between reads. It stops after five minutes. It respects Retry-After and repeats temporary failed reads. It never submits or retries work. If polling stops, keep the ID and run it again for the same job.
The example returns the full job on success or partial success. It prints the job and exits with an error for failed, canceled, or review states. An accepted transcript job can stay running with counts.pending=1 during an automatic retry. Continue to read that job. Sovran sends no terminal webhook during this wait.
Read job responses
These examples show sequence render jobs. Other job types have their own output fields. HTTP 200 means the read succeeded; the job's status tells you whether the work succeeded.
Success
{
"data": {
"id": "22222222-2222-4222-8222-222222222222",
"type": "sequence_render",
"status": "succeeded",
"progress": 100,
"counts": { "total": 1, "succeeded": 1, "failed": 0, "pending": 0 },
"outputs": [{ "renderId": "44444444-4444-4444-8444-444444444444", "aspectRatio": "9:16", "status": "ready", "error": null }],
"error": null,
"createdAt": "2026-10-03T10:00:00.000Z",
"completedAt": "2026-10-03T10:01:00.000Z"
},
"requestId": "00000000-0000-4000-8000-000000000001"
}Partial success
{
"data": {
"id": "22222222-2222-4222-8222-222222222222",
"type": "sequence_render",
"status": "partially_succeeded",
"progress": 50,
"counts": { "total": 2, "succeeded": 1, "failed": 1, "pending": 0 },
"outputs": [
{ "renderId": "44444444-4444-4444-8444-444444444444", "aspectRatio": "9:16", "status": "ready", "error": null },
{ "renderId": "55555555-5555-4555-8555-555555555555", "aspectRatio": "9:16", "status": "failed", "error": { "code": "render_failed", "message": "The render failed. Check the source and settings before retrying." } }
],
"error": null,
"createdAt": "2026-10-03T10:00:00.000Z",
"completedAt": "2026-10-03T10:01:00.000Z"
},
"requestId": "00000000-0000-4000-8000-000000000001"
}Failure
{
"data": {
"id": "22222222-2222-4222-8222-222222222222",
"type": "sequence_render",
"status": "failed",
"progress": 0,
"counts": { "total": 1, "succeeded": 0, "failed": 1, "pending": 0 },
"outputs": [{ "renderId": "55555555-5555-4555-8555-555555555555", "aspectRatio": "9:16", "status": "failed", "error": { "code": "render_failed", "message": "The render failed. Check the source and settings before retrying." } }],
"error": null,
"createdAt": "2026-10-03T10:00:00.000Z",
"completedAt": "2026-10-03T10:01:00.000Z"
},
"requestId": "00000000-0000-4000-8000-000000000001"
}Read child errors even when the top-level error is null. A job read also checks current output availability. Missing completed output can change a job to requires_action. HTTP 503 with output_unavailable means the check could not finish. Repeat the read before you submit more work.
Resolve a blocked job
Save the job ID, request ID, job type, top-level error, and child errors. Check the matching project, billing state, and provider connection in the dashboard.
| Error or state | Your next action |
|---|---|
charge_unconfirmed | Ask the workspace owner to check the original charge. Contact Sovran support with the saved IDs if it remains uncertain. Do not submit replacement work. |
submission_unconfirmed, submission_unknown, or manual_review | Check the provider result first. For Meta publishing, check Meta Ads Manager. Ask Sovran support to resolve an uncertain outcome. Do not create a second provider request. |
output_unavailable in a job or child result | Check the completed output in Sovran. Contact support if it is missing. A new submission can create another charge. |
refund_pending from a retry request | Wait for the original refund. Read the existing job and billing state before you try the retry again. |
retry_not_allowed or retry_limit | Stop the retry loop. Check the workflow's source, consent, state, and limits. Some workflows require a new source or request. |
job_state_changed from a read | Read the same job again. Its state changed during the check. |
api_disabled or stage_unavailable | Check API availability on Developer. Keep the saved job ID. Resume reads when access is available. |
There is no universal retry-eligibility field. The retry route checks the current job, permissions, paid plan, charge, refund, source availability, and workflow limits.
Retry confirmed failed work
Only request a retry after you resolve the cause and confirm that the workflow supports it. The current key needs the original operation's permissions and a current paid plan. A revoked key cannot retry. Failed paid work must resolve its original charge and refund first.
# Create this value once for this retry. Keep it if the response is lost.
export SOVRAN_RETRY_KEY="$(node --input-type=module -e 'import { randomUUID } from "node:crypto"; console.log(randomUUID())')"
curl --fail-with-body "$SOVRAN_API_ORIGIN/api/v1/jobs/JOB_UUID/retry" \
-X POST \
-H "Authorization: Bearer $SOVRAN_API_KEY" \
-H "Idempotency-Key: $SOVRAN_RETRY_KEY" \
-H "Content-Type: application/json" \
--data '{}'Use a new idempotency key for a new retry. If the response is lost, repeat the same request with the same key. Save the returned data.jobId and poll that job. An accepted retry can create a new charge.
A sequence render batch retries only confirmed failed children. Successful children are kept. It permits at most three explicit retries in one parent chain. Other workflows have their own limits. A transcript job with an automatic retry pending rejects an explicit retry until terminal failure. An uncertain charge or provider result is not an automatic retry.
Read credits
Read GET /usage for the workspace's paid plan status and feature balances. Each feature includes monthlyRemaining, bankedRemaining, totalAvailable, and unlimited. Monthly usage, included credits, and the next reset time appear when the billing account provides them. A valid key can read these values after plan expiry.
When unlimited is true, the feature has an unlimited entitlement. Its numeric balance can be zero. Do not treat that zero as exhausted credits. When unlimited is false, the response reports the available finite credits. New work still checks the current paid plan and feature access before acceptance.
Set up webhooks
Open Developer. Add a public HTTPS destination and select job events. Sovran shows its signing secret once. Store your copy on the receiving server. Delivery results appear on the Developer page.
Events are job.succeeded, job.failed, job.partially_succeeded, and job.requires_action. The event ID identifies one event. Save it to detect duplicate deliveries. Return a 2xx response only after you save the event to durable storage.
Sovran saves the event with the job result. The event body, ID, and createdAt stay the same on retry. Each delivery gets a fresh signature timestamp in the Sovran-Signature header. Apply the five-minute check to that header timestamp, not the event's createdAt. A delivery two hours later can still have a valid fresh signature. A new webhook destination does not receive old job events.
Verify before parsing
Sovran-Signature has t=TIMESTAMP,v1=HEX_DIGEST. The timestamp is Unix seconds. The digest is HMAC-SHA256 of TIMESTAMP + "." + RAW_BODY. Read the exact bytes before parsing JSON. Reject a missing or malformed header. Reject timestamps more than five minutes before or after your server's current time. Keep the server clock correct.
import { createHmac, timingSafeEqual } from 'node:crypto'
function verifySignature(rawBody, header, secret, now = Math.floor(Date.now() / 1000)) {
if (!Buffer.isBuffer(rawBody) || typeof header !== 'string' || typeof secret !== 'string' || !secret || !Number.isFinite(now)) return false
const match = /^t=(\d{1,12}),v1=([a-f0-9]{64})$/i.exec(header)
if (!match) return false
const timestamp = Number(match[1])
if (!Number.isSafeInteger(timestamp) || Math.abs(now - timestamp) > 300) return false
const expected = createHmac('sha256', secret).update(`${match[1]}.`).update(rawBody).digest()
const supplied = Buffer.from(match[2], 'hex')
return supplied.length === expected.length && timingSafeEqual(supplied, expected)
}Run a complete receiver
Download the webhook receiver. It needs Node.js 22 or later and no extra packages. It verifies the raw body and timestamp. It writes each event to a durable file inbox before it returns 204. Repeated event IDs keep the first saved body and return 204 again.
curl --fail --output webhook-receiver.mjs "$SOVRAN_API_ORIGIN/docs/api/webhook-receiver.mjs"
export SOVRAN_WEBHOOK_INBOX="/absolute/path/to/persistent/sovran-events"
# Set SOVRAN_WEBHOOK_SECRET through your secret store.
node webhook-receiver.mjsThe receiver listens on 127.0.0.1:8787/webhooks/sovran. Route your public HTTPS endpoint to that address. Use a persistent local filesystem that supports atomic hard links and directory sync, such as a Linux or macOS server disk. An ephemeral serverless disk is not suitable. Limit access to the inbox and signing secret.
Process the saved JSON files with your own worker. Use the event ID to prevent duplicate downstream actions. Keep the event ID record after processing; deleting it removes duplicate protection. The receiver saves receipt only. It does not start paid jobs or perform provider actions. For multiple server hosts, use a shared durable database inbox with a unique event-ID constraint instead.
There are five delivery attempts. The four retry delays are 1, 5, 30, and 120 minutes. Each attempt has a 10-second timeout. Sovran validates the address for each attempt. Redirects, private networks, non-HTTPS addresses, and non-standard ports are blocked. Failed delivery does not rerun the job or create another charge.