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
| Status | Meaning | Terminal |
|---|---|---|
queued | Sume accepted the request and the job is waiting to run. | No |
processing | The job is running or being finalized. | No |
completed | The result is ready. | Yes |
failed | The job reached a terminal failure with a public error. | Yes |
canceled | Cancellation 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 optionalwait_for: "all" | "any"(defaultall); response isobject: "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.createdjob.startedprovider.submittedjob.completedjob.failedjob.canceledwebhook.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.
| Mode | HTTP returns | Job id in the first response | Server blocks | What the client does next |
|---|---|---|---|---|
async (default) | 202 with the job envelope and polling URLs | Yes | No | Poll status_url until terminal is true, then GET result_url once result_ready is true. |
sync | The same envelope, after waiting up to wait_timeout_seconds (max 30) for a terminal transition | Yes | Yes, at most 30s — less when waiter capacity is unavailable | Terminal? Read the job off the response. Not terminal? Poll. Do not resubmit. |
subscribe | Identical to sync — the same bounded wait | Yes | Same as sync | Same as sync. For a fal-style long wait, use the client subscribe recipe below with async. |
webhook | 202 with the job envelope and polling URLs; the callback is stored | Yes | No | Wait 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 it | What it is | How 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.mode | Delivery 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 waitsyncgets. For progress, submitasyncand readGET /v1/jobs/:id/events, or take a webhook. communication.modehas nosubscribevalue at all, and its two values behave identically — supplyingwebhook_urlis 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:
- The response is still
2xxand still carries the job id. Wait exhaustion is not an admission failure. - The envelope carries
status_url,result_url,events_url,cancel_url, and asyncobject:sync.timed_outis true when the wait returned before a terminal state, andsync.capacity_exhaustedis true when Sume skipped the wait because the per-process waiter budget was full. - You must continue with
GET status_url, honoringnext_poll_after_secondswhen it is present and backing off otherwise. - You must not submit a new paid job for the same intent. Retrying the
submit itself is fine — reuse the same
Idempotency-Keyso 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.