Jobs and results

Sume generation endpoints create durable jobs. Store the job id from submit responses so your integration can recover work after process restarts.

For paid generation, queued is a normal accepted state. Workspace concurrency limits apply when workers move jobs into processing, not when the API accepts valid jobs. See Generation admission for queue capacity, tier limits, and queue-full errors.

Statuses

StatusMeaningTerminal
queuedSume accepted the request and the job is waiting to run.No
processingThe job is running or being finalized.No
completedThe result is ready.Yes
failedThe job reached a terminal failure with a public error.Yes
canceledCancellation was requested and the job is terminal.Yes

Poll status

Use exponential backoff and stop polling on completed, failed, or canceled. Do not resubmit the original paid request just because a local process timed out.

jobs_wait

Remote MCP jobs_wait accepts either:

  • job_id — single-job wait; response shape is unchanged (object: "job_wait")
  • job_ids — 1–400 ids with optional wait_for: "all" | "any" (default all); response is object: "job_wait_batch" with a status snapshot for every requested id

Prefer one batch wait per short slice after parallel fan-outs instead of N single waits. On timeout, retry jobs_wait with the same ids — never resubmit the paid create. wait_for: "any" still reports every id; remaining jobs continue and still bill. Unknown or foreign-workspace ids fail the whole call.

Slices are bounded, and the server enforces it

timeout_seconds defaults to 100 and is capped at 120. Stills are the one class worth shortening to 50 — not for speed, since a wait returns the moment its job is terminal, but because an image still running at 50s is usually stuck rather than slow.

A wait is one HTTP request held open for the whole slice with nothing transferring, and every edge closes such a request eventually — the caller then gets no tool result at all while the job keeps running and keeps billing. A larger timeout_seconds is clamped rather than rejected, and the response says so in wait_slice_clamped.

Wait for a ten-minute render by repeating the wait, not by asking for a longer one. A 524 (or 522 / 523 / 525) on jobs_wait is a transport failure, never a job outcome: re-issue jobs_wait on the same ids, or read jobs_status once. Do not resubmit the paid create and do not report the job blocked.

Fetch a result

Results are available only after completion. If the job is not complete yet, the API returns a conflict response instead of pretending the result is empty.

Reading a whole wave (MCP)

Over MCP, jobs_result also takes job_ids — the same 1–400 ceiling as jobs_wait, so a wave you waited on in one call reads back in one call instead of N:

The response is a job_result_batch: results[] in request order, one entry per id, each with ok plus either value or a typed error. Partial success is normal and deliberate — an id that is still running comes back as job_not_completed while every finished id still returns its result, and partial_failure.failed_job_ids names exactly the ids worth re-reading. Read ok per entry; a failure on one id says nothing about the others.

Read events

Events provide a public timeline for debugging and recovery:

  • job.created
  • job.started
  • provider.submitted
  • job.completed
  • job.failed
  • job.canceled
  • webhook.delivery

Public events do not expose raw provider task ids or raw provider URLs.

Request cancellation

Cancellation is available for queued or processing jobs. A job may still finish if provider execution is already past the point where cancellation can stop it.

Communication modes

Every submit endpoint accepts a mode. The mode decides how you learn the outcome. It never changes whether a job is created, what it costs, or how long the job takes to run.

ModeHTTP returnsJob id in the first responseServer blocksWhat the client does next
async (default)202 with the job envelope and polling URLsYesNoPoll status_url until terminal is true, then GET result_url once result_ready is true.
syncThe same envelope, after waiting up to wait_timeout_seconds (max 30) for a terminal transitionYesYes, at most 30s — less when waiter capacity is unavailableTerminal? Read the job off the response. Not terminal? Poll. Do not resubmit.
subscribeIdentical to sync — the same bounded waitYesSame as syncSame as sync. For a fal-style long wait, use the client subscribe recipe below with async.
webhook202 with the job envelope and polling URLs; the callback is storedYesNoWait for the terminal callback, verify its signature, and keep polling as a backup.

Omit mode and you get async. Send webhook_url (or its alias callback_url) without a mode and you get webhook.

A submit is accepted the moment Sume has a durable job id, so every mode returns the job id in its first response. A 2xx means the job exists and paid work is in flight — it does not mean the job finished. Read terminal and result_ready off the envelope to tell those apart.

and

They run the same bounded waiter and return the same envelope. subscribe is kept because clients ported from other queue APIs reach for it; it is not a long-lived subscription, an event stream, or a longer wait. There is no SSE or WebSocket transport on the Developer API today — GET /v1/jobs/:id/events is a pull snapshot, not a stream.

"Subscribe" means three different things

The word appears on three unrelated surfaces with three different waits. None of them is a push stream. Read the table before wiring a timeout.

Where you see itWhat it isHow long it waits
Job mode: "subscribe" (this page)An alias of sync. One bounded HTTP wait on the submit call.At most wait_timeout_seconds, capped at 30s.
SDK subscribeFormatRun() (TypeScript SDK)Create the run, then poll it client-side to a terminal receipt.Minutes — the SDK's own timeout, not an HTTP hold.
Format / Action / Agent communication.modeDelivery selection for a run, not a job. The values are async and webhook; subscribe is not one of them.Nothing blocks.

Two consequences worth spelling out:

  • Sending mode: "subscribe" does not get you progress events. It gets you the same 30-second wait sync gets. For progress, submit async and read GET /v1/jobs/:id/events, or take a webhook.
  • communication.mode has no subscribe value at all, and its two values behave identically — supplying webhook_url is what actually arms delivery.

New integrations should reach for async (poll, or read events) or webhook (be told). sync and subscribe remain supported and are not going anywhere; they are just the wrong tool for anything that can outlast 30 seconds, which is most video work.

30 seconds is a wait budget, not a job duration

wait_timeout_seconds is clamped to 0..30. It bounds how long the HTTP request blocks, not how long the job may take. Image jobs often finish inside it. Video, avatar-video, and face-swap jobs routinely do not.

When the budget runs out — or when the API process has no waiter capacity left and skips the blocking wait entirely:

  1. The response is still 2xx and still carries the job id. Wait exhaustion is not an admission failure.
  2. The envelope carries status_url, result_url, events_url, cancel_url, and a sync object: sync.timed_out is true when the wait returned before a terminal state, and sync.capacity_exhausted is true when Sume skipped the wait because the per-process waiter budget was full.
  3. You must continue with GET status_url, honoring next_poll_after_seconds when it is present and backing off otherwise.
  4. You must not submit a new paid job for the same intent. Retrying the submit itself is fine — reuse the same Idempotency-Key so the retry returns the original job instead of billing a second one.

sync is null on async and webhook responses.

Client subscribe: poll a job to a terminal state

This is the official equivalent of a client-side subscribe(), and the right answer for anything that can outlast 30 seconds. Submit with async, poll, then read the result. The wait lives in your client, so its timeout can be minutes without holding an HTTP request open.

Poll on the booleans (terminal, result_ready) or on sume_status. The status endpoint also returns a queue-shaped status field (IN_QUEUE / IN_PROGRESS / COMPLETED / FAILED / CANCELED) that maps one-to-one onto sume_status for clients ported from other queue APIs. The two never disagree, but do not mix them.

GET /v1/jobs/:id/result is only for completed jobs — it answers 409 job_not_completed otherwise, so read the failure off the job record instead.

Submit (step 1):

The response carries request_id (the job id), status_url, result_url, and next_poll_after_seconds.

Poll (step 2), until "terminal": true:

Fetch (step 3), once "result_ready": true:

Any HTTP client can run this loop. In Kotlin with OkHttp or Ktor the shape is:

A client-side timeout does not cancel the job. The job keeps running and still bills; you have only stopped watching. Store the job id and pick it back up from status_url, or cancel it explicitly.

In TypeScript, waitForJob from @sume-com/sdk is this loop.

Job webhooks are terminal-only

mode: "webhook" delivers exactly three events — job.completed, job.failed, and job.canceled — to a public HTTPS webhook_url. There are no progress or partial webhooks. Sume signs the raw body with HMAC SHA-256 over {timestamp}.{raw_body} and sends x-sume-webhook-timestamp plus x-sume-webhook-signature: sume-v1=…. See Webhooks for the payload and a verifier.

A webhook is a delivery optimization, not your only recovery path — keep status_url polling available for missed or retried deliveries.

Action, Format, and Agent Completion runs are a different surface with their own *.run.terminal events; see Run webhooks.

Idempotency

Send Idempotency-Key on submit requests when retrying after client-side timeouts or network failures.

Reuse the same key only for the same operation and payload.

Result shape

Completed jobs can include artifacts:

Use Sume media URLs from the result. Raw provider URLs are not public API outputs.