Runs and results
POST …/runs answers immediately with a receipt; the run itself takes minutes. This page is
about what happens after the 202: how you learn the run finished, and what the finished
receipt contains.
There are two ways to learn, and they carry the identical receipt:
| Webhook | Poll | |
|---|---|---|
| You do | Send communication.webhook_url on the create, verify the signature, answer 2xx. | Read the run until status is terminal. |
| You get | One signed POST per run, when it completes or fails. | The same receipt, on your schedule. |
| Costs you | One public HTTPS endpoint. | One timer per in-flight run, and read budget. |
| Available | api.dev.sume.com and api.sume.com. | Everywhere. |
Production integrations do both: the webhook is the fast path, and a read of result_url is
the backup for the day your endpoint is down. Nothing about the run changes when a delivery
fails.
Lifecycle
| Status | Meaning |
|---|---|
queued | Accepted, not started. |
processing | The run is working. |
completed | Finished. output, artifacts[] and primary_output_url are populated. |
failed | Finished with an error. error says why; artifacts[] still carries whatever was made. |
canceled | Stopped by POST …/cancel. One l. |
skipped | Never ran: you sent on_active_run: "skip" and another run was in flight. |
next_action on the receipt says what to do: poll_status while queued or processing,
retry_later on a skipped run, none on every terminal run. Those are the only three
values a Format run emits.
Poll
The receipt carries its own URLs; follow them rather than building paths.
| URL on the receipt | Endpoint | Returns |
|---|---|---|
| (the receipt itself) | GET /v1/format-runs/{run_id} | The full receipt at any status. Most integrations poll this one. |
status_url | GET /v1/format-runs/{run_id}/status | The small poll payload: status, next_action, cancelable, expires_at, queue, timestamps and the URLs again. Once terminal it also carries usage and, on a failure, error, but never output, artifacts or primary_output_url. |
result_url | GET /v1/format-runs/{run_id}/result | The full receipt once terminal; 409 run_not_completed with details.status while the run is in flight. |
events_url | GET /v1/format-runs/{run_id}/events | The phase timeline. See Watch a run progress. |
cancel_url | POST /v1/format-runs/{run_id}/cancel | Stops the run. See Cancel. |
A loop that reads the full receipt and stops on a terminal status:
Three habits keep a poll loop honest:
- Back off. Long-form video is 15 to 30 minutes of work; polling every second buys nothing
and spends read budget. Double the gap up to a minute. A
429or503during the loop is transient: the run is still executing and still spending, so wait and poll again rather than treating it as a failed run. - Use
expires_atas your ceiling. A non-terminal receipt carries the deadline past which the run is force-finalized asfailed: 90 minutes fromcreated_at, or sooner when the run is older than 25 minutes and has been silent for 10. Set your own timeout from it rather than inventing one. It isnullonce the run is terminal. - Read
queueifqueuedlasts.queue.stateiswaitingwhile pickup is inside the normal window andruntime_unavailableonce the run has waited past it with nothing claiming it; thenretry_after_secondssays how long to back off.positionis alwaysnull: Sume does not publish queue depth. Aruntime_unavailablethat lasts more than a few minutes is worth a support ticket with therequest_id.
In TypeScript, subscribeFormatRun creates the run and runs this loop for you;
waitForRun does the loop for a run id you already have.
Webhook
Send communication.webhook_url on the create and Sume POSTs the terminal receipt to it once,
when the run completes or fails, on both api.dev.sume.com and api.sume.com. A canceled or
skipped run never delivers: cancel answers you directly, and a skipped run is already terminal
on the create response.
| Field | Branch on it for |
|---|---|
event | Always format.run.terminal for a Format run. Route on it without inspecting the body. |
request_id, run_id | Equal, and stable across retries: the dedupe key. |
status | OK when the run completed, ERROR when it failed. |
outcome | ok: completed with output. degraded: completed and billed, real media in artifacts[], but output is null because the projection did not match your schema, and output_error says why. error: the run did not complete. Branch here when the question is "did I get usable output". |
payload | The run receipt, byte-identical to data from GET /v1/format-runs/{run_id}, so one handler serves both transports. null only when the receipt was over 1 MiB; then error.code is payload_too_large and error.result_url says where to fetch it. |
error | null on OK; otherwise { code, message }, mirroring payload.error. |
created_at | When this delivery body was built. Order deliveries by it; request_id cannot, since it repeats on retries. |
Verify every delivery
Every POST carries three headers:
The signature is HMAC-SHA256 over <timestamp>.<raw_body> with your workspace's signing
secret, which you read on the dashboard's Webhooks tab or from
GET /v1/webhooks/signing-secret (any key with account:read). Verify against the raw bytes
before parsing, reject timestamps outside a five-minute window, and compare the fingerprint
header with the one shown next to the secret to confirm both sides hold the same one. The
Cookbook has complete receivers in Node and Python;
in TypeScript the check is one call:
Delivery rules
| Property | Value |
|---|---|
| When | Once per run, on completed or failed. Never on canceled or skipped. |
| Success | Any 2xx, within 10 seconds. Record the event durably, answer, then do the work. |
| Retries | Up to 10 attempts. Backoff is the longer of exponential (30 s × 2^(attempt−1), with jitter) and your Retry-After on a 429/503, capped at one hour. |
| Redirects | Not followed. A 3xx is a failed attempt, so register the final URL. |
| URL rules | Public HTTPS only. Localhost, private ranges, credentials in the URL and plain HTTP are 400 invalid_request at create, and the URL is checked again at delivery time. |
| Dedupe | On request_id; every retry repeats it. |
A delivery outcome never changes the run. Ten refused attempts leave you with a failed
delivery and a run that is still completed. Fetch it from result_url.
Check what happened to a delivery
Every receipt for a run created with a webhook_url carries a webhook_delivery block:
status | Means |
|---|---|
not_armed | The URL is stored and nothing is scheduled yet: the run is still going. |
pending, retrying | Armed; next_attempt_at is when the next attempt is due. |
delivered | Your endpoint answered 2xx. |
failed, exhausted | Sume gave up. last_status_code and last_error (our transport error, never your body) say why. The run is unchanged. |
To replay a terminal delivery, after fixing your receiver or to test one, call
POST /v1/format-runs/{run_id}/webhook/redeliver with formats:write and an empty body. It
re-POSTs the current receipt with a fresh timestamp and signature and does not consume one of
the automatic ten. 409 webhook_not_configured when the run had no URL,
409 run_not_terminal while it is still running.
The full contract, including the generation-job webhooks that POST /v1/models/… emits on a
different event set, is on Run webhooks.
Watch a run progress
status says whether a run is done. GET /v1/format-runs/{run_id}/events says what it is
doing:
preparing is everything before the agent has the run in hand; running is the agent working
the recipe, which is where the time goes; finalizing is teardown and output harvest. Each
entry carries a status (pending, running, done, warning, error, skipped) and a
duration_ms when the phase measured itself. Consecutive entries with the same phase and
status collapse into one whose at keeps advancing, so at on the last entry is the run's
progress clock. If it stops moving for several minutes the run is stalled, not slow, and will
be finalized at the bounds above.
It is a phase timeline, not a log stream. Agent output, tool calls and sandbox internals are not published here and will not be. There is no push channel for progress: poll this endpoint, or skip it and take the terminal webhook.
The run receipt
GET /v1/format-runs/{run_id} returns the full shape at any status.
| Field | Notes |
|---|---|
id, object | arun_…, and format.run. |
format | { id, slug, title, version }. id is the opaque skl_…; version is the Format version that actually ran, unchanged by later edits. |
status, next_action, cancelable | See Lifecycle. cancelable is true while queued or processing. |
trigger | { source: "api", idempotency_key } for every run you create. |
created_at, started_at, finished_at | The last two are null until they happen. |
expires_at, queue | The deadline and the pickup state. See Poll. |
output_schema | { name, strict, source }: which schema shaped output. source is request_override when you sent one. |
output | The structured result. null on every non-terminal status, and null when nothing satisfied output_schema. A failed run still publishes whatever partial result it authored. |
output_error | { code, message, details } when output could not be produced. Check it before reading output. |
primary_output_key, primary_output_url | The one thing to show. Both null unless completed. |
artifacts[] | Every durable file the run generated: { id, type, url, content_type, size_bytes, width, height, duration_ms, checksum_sha256 }. Empty until terminal; populated on failures too. |
usage | { currency, billable_amount_usd_micros, generation_spend_cap_usd_micros }. See Reading usage. null when spend could not be read. |
error | { code, message }. Non-null only when failed. |
skip_reason | Set on a skipped run. |
webhook_delivery | Delivery state for the URL you registered, or null if you registered none. |
idempotency_hit | true when this receipt is an idempotency replay rather than a new run. |
thread_id, previous_run_id | The conversation this turn happened in, and the run it continued. See Continue a run. |
model | The catalog id the orchestrator ran on. |
request_id | Log it; it is what support asks for. |
status_url, result_url, events_url, cancel_url | See Poll. |
Media URLs are durable media.sume.com HTTPS URLs. They do not expire, and they are public to
anyone holding the URL, so proxy or copy them if your product needs per-customer access
control.
Continue a run
A Format run is one agent turn. Send previous_run_id on a new POST …/runs and the next
turn continues the same conversation: the agent is replayed what it produced, so it can redo
one part and leave the rest alone. This is how live-commerce integrations retry a single scene
without paying for the whole show again.
The run you name must be continuable, which you can read off its own receipt: thread_id is
not null, and it either completed or has a non-empty artifacts[]. A failed run that
left work behind can be continued; one that left nothing cannot.
| Refusal | Means |
|---|---|
404 previous_run_not_found | Unknown id, or another owner's run. |
400 previous_run_format_mismatch | That run was created on a different Format. Continue it where it started. |
409 previous_run_not_terminal | It has not finished. Poll it, then call again. |
400 previous_run_not_resumable | Nothing to continue: no thread_id, or neither completed nor any artifacts. details reports previous_run_status, has_thread and artifact_count. Start a fresh run. |
A continuation is a new run: new id, new receipt, its own spend cap, its own single webhook.
The original run never changes. Both share thread_id, which is read-only. Continue by naming
previous_run_id, never by sending a thread id (that is 400 unknown_parameter). Bind the
same output_schema every turn; it is per run, not inherited. artifacts[] on a continued
run lists everything the whole conversation generated, while usage stays per run.
Cancel
Needs formats:write, and is idempotent: the current receipt comes back either way, and
cancel_effect says which happened. canceled means this call stopped a run in flight;
no_op means it had already finished. Generation the run completed before the cancel is
billed, and usage reports it.
A canceled run never delivers a webhook. If your integration is webhook-only, cancel is the one path where nothing will arrive; use the receipt this call returns.
List runs for a Format
Newest first; limit is 1–100 and defaults to 20; GET /v1/formats/{format_id}/runs is the
opaque twin. A Format that has never been run over the API returns an empty list, not a
404. The response is a page: pass next_cursor back as cursor until has_more is
false. The cursor is opaque and keyset over (created_at, id), so runs created while you
page do not shift rows; a cursor that is not ours is 400 invalid_request.
Routes that do not exist
Three paths callers guess at, and what to use instead:
| Guess | Use |
|---|---|
GET /v1/format-runs | There is no cross-Format list. List per Format, or keep your own index keyed by the data.id you stored at create. |
GET /v1/formats/{handle}/{slug}/runs/{run_id} | Runs are read at /v1/format-runs/{run_id}. The Format path only creates and lists. |
GET /v1/format-runs/{run_id}/messages | The conversation is not published over the API. events_url gives the phase timeline; output and artifacts[] carry the result. |
usage
usage.billable_amount_usd_micros is the generation spend attributed to this run: the running
total the run's cap is enforced against, counting both reserved and captured amounts. It climbs
while the run is in flight and settles when it terminates. It excludes the agent's own LLM
turn, so it is not the run's total cost, and it is a receipt figure rather than an invoice:
GET /v1/usage is the authoritative billing record. usage is null
when spend could not be read at all, which is different from 0.
Errors on the run endpoints
| Status | error.code | What to do |
|---|---|---|
| 401 | unauthorized | Missing, malformed, revoked or unknown key. |
| 403 | insufficient_scope | The key lacks formats:read (reads) or formats:write (cancel, redeliver). Mint a new key. |
| 404 | format_run_not_found | Unknown run id, or another owner's run. A run you cannot see reads the same as one that does not exist. |
| 409 | run_not_completed | GET …/result before the run is terminal. details.status carries the current status; poll status_url and retry. |
| 429 | rate_limited | Wait retry-after. Polling spends the read budget, which is separate from and far larger than the write one. |
| 503 | studio_agent_upstream_unavailable | A Sume-side outage, not your key. Retry later; the run is still executing. |
Every code, with the run-time failures (error and output_error) alongside, is on
Errors and spend.
Next
- Errors and spend: every code, what a
failedrun carries, credits and rate limits - Structured output: shaping
output, and what to do when it is null - Cookbook: a webhook receiver, a scene retry, a batch
- Run webhooks: the complete delivery contract
- Waiting for runs:
subscribeFormatRun,waitForRun, and the phase timeline from TypeScript

