Sovran
API documentation

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.

StatusYour next action
queuedKeep reading the same job. Its inputs are saved.
runningKeep reading the same job. Do not submit it again.
succeededUse its output references. For renders, read GET /renders/{renderId}/outputs to get a temporary download URL.
partially_succeededKeep the successful outputs. Read the failed child errors before you request a retry.
failedRead error and any child errors. Resolve the cause before you request a supported retry.
requires_action or manual_reviewStop automatic submission. Follow Resolve a blocked job.
canceledStop 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.

Poll an accepted job
curl --fail --output poll-job.mjs "$SOVRAN_API_ORIGIN/docs/api/poll-job.mjs"
node poll-job.mjs JOB_UUID

Replace 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

GET /api/v1/jobs/{jobId} — all renders succeeded
{
  "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

GET /api/v1/jobs/{jobId} — keep the ready output
{
  "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

GET /api/v1/jobs/{jobId} — read the child error
{
  "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 stateYour next action
charge_unconfirmedAsk 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_reviewCheck 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 resultCheck the completed output in Sovran. Contact support if it is missing. A new submission can create another charge.
refund_pending from a retry requestWait for the original refund. Read the existing job and billing state before you try the retry again.
retry_not_allowed or retry_limitStop 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 readRead the same job again. Its state changed during the check.
api_disabled or stage_unavailableCheck 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.

POST /api/v1/jobs/{jobId}/retry — after failure review
# 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.

Verify the signature and its five-minute window
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.

Start the receiver behind your HTTPS server
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.mjs

The 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.

Was this article helpful?

On this page