Runs and results

POST …/runs answers immediately with a receipt. The run takes minutes. This page is about the time after the 202. It tells you how to know that the run finished, and what the finished receipt contains.

There are two ways to know the result, and the two ways give the same receipt:

WebhookPoll
You doSend communication.webhook_url on the create. Verify the signature. Answer 2xx.Read the run until status is terminal.
You getOne signed POST for each run, when it completes or fails.The same receipt, on your schedule.
Costs youOne public HTTPS endpoint.One timer for each run in progress, and read budget.
Availableapi.dev.sume.com and api.sume.com.On all hosts.

Production integrations use the two ways. The webhook is the fast path. A read of result_url is the backup for the day when your endpoint is down. When a delivery fails, the run does not change.

Lifecycle

StatusMeaning
queuedAccepted, not started.
processingThe run is in progress.
completedFinished. output, artifacts[], and primary_output_url are populated.
failedFinished with an error. error gives the cause. artifacts[] still holds all the media that the run made.
canceledPOST …/cancel stopped the run. The word has one l.
skippedThe run never started: you sent on_active_run: "skip", and a different run was in progress.

next_action on the receipt tells you what to do. It is poll_status while the run is queued or processing, retry_later on a skipped run, and none on each terminal run. Those are the only three values that a Format run emits.

Poll

The receipt holds its own URLs. Use these URLs. Do not build the paths yourself.

URL on the receiptEndpointReturns
(the receipt itself)GET /v1/format-runs/{run_id}The full receipt at all statuses. Most integrations poll this URL.
status_urlGET /v1/format-runs/{run_id}/statusThe small poll payload: status, next_action, cancelable, expires_at, queue, timestamps, and the URLs again. When the run is terminal, it also holds usage, and error on a failure. It never holds output, artifacts, or primary_output_url.
result_urlGET /v1/format-runs/{run_id}/resultThe full receipt when the run is terminal. While the run is in progress, it returns 409 run_not_completed with details.status.
events_urlGET /v1/format-runs/{run_id}/eventsThe phase timeline. Refer to Watch a run progress.
cancel_urlPOST /v1/format-runs/{run_id}/cancelStops the run. Refer to Cancel.

A loop that reads the full receipt and stops on a terminal status:

Use these three practices to keep a poll loop correct:

  • Back off. Long-form video is 15 to 30 minutes of work. Thus, a poll each second gives you nothing and uses read budget. Double the gap, up to one minute. A 429 or 503 during the loop is temporary, because the run continues to execute and to spend. Thus, wait and poll again, and do not think that the run failed.
  • Use expires_at as your ceiling. A non-terminal receipt holds the deadline after which Sume force-finalizes the run as failed. The deadline is 90 minutes from created_at, or earlier when the run is older than 25 minutes and was silent for 10. Use this value to set your own timeout, and do not invent a different one. When the run is terminal, it is null.
  • Read queue if queued lasts. queue.state is waiting while the pickup is in the normal window. It is runtime_unavailable when the run waited longer than that window and nothing claimed it. Then retry_after_seconds tells you how long to back off. position is always null, because Sume does not publish the queue depth. If a runtime_unavailable lasts more than a few minutes, send a support ticket with the request_id.

In TypeScript, subscribeFormatRun creates the run and runs this loop for you. waitForRun does the loop for a run id that you already have.

Webhook

If you send communication.webhook_url on the create, Sume POSTs the terminal receipt to it one time. This occurs when the run completes or fails, on api.dev.sume.com and on api.sume.com. A canceled or skipped run never delivers. The cancel call answers you directly, and a skipped run is already terminal on the create response.

FieldBranch on it for
eventAlways format.run.terminal for a Format run. Route on it. You do not need to examine the body.
request_id, run_idEqual, and stable across retries. They are the dedupe key.
statusOK when the run completed, ERROR when it failed.
outcomeok: completed with output. degraded: completed and billed, with real media in artifacts[]. But output is null because the projection did not match your schema, and output_error gives the cause. error: the run did not complete. Branch here when the question is "did I get usable output".
payloadThe run receipt. It is byte-identical to data from GET /v1/format-runs/{run_id}, thus one handler serves the two transports. It is null only when the receipt was more than 1 MiB. Then error.code is payload_too_large, and error.result_url gives the address to fetch it.
errornull on OK. On other statuses, { code, message }, the same as payload.error.
created_atThe time when Sume built this delivery body. Use it to put deliveries in order. You cannot use request_id for the order, because it repeats on retries.

Verify every delivery

Each POST has three headers:

The signature is HMAC-SHA256 over <timestamp>.<raw_body> with the signing secret of your workspace. You can read this secret on the Webhooks tab of the dashboard, or from GET /v1/webhooks/signing-secret (each key with account:read can read it).

Before you parse the body, verify the signature against the raw bytes. Reject timestamps outside a five-minute window. Compare the fingerprint header with the fingerprint next to the secret. This makes sure that the two sides hold the same secret. The Cookbook has complete receivers in Node and Python. In TypeScript, the check is one call:

Delivery rules

PropertyValue
WhenOne time for each run, on completed or failed. Never on canceled or skipped.
SuccessAll 2xx codes, in not more than 10 seconds. Record the event in durable storage. Then answer. Then do the work.
RetriesUp to 10 attempts. The backoff is the longer of two values: exponential (30 s × 2^(attempt−1), with jitter), or your Retry-After on a 429/503. The maximum backoff is one hour.
RedirectsSume does not follow redirects. A 3xx is a failed attempt, thus register the final URL.
URL rulesPublic HTTPS only. Localhost, private ranges, credentials in the URL, and plain HTTP give 400 invalid_request at create. Sume examines the URL again at delivery time.
DedupeOn request_id. Each retry repeats it.

A delivery outcome never changes the run. After ten refused attempts, you have a failed delivery and a run that is still completed. Fetch the run from result_url.

Check what happened to a delivery

Each receipt for a run created with a webhook_url has a webhook_delivery block:

statusMeans
not_armedSume stored the URL, and no delivery is scheduled yet. The run is still in progress.
pending, retryingArmed. next_attempt_at is the time of the next attempt.
deliveredYour endpoint answered 2xx.
failed, exhaustedSume stopped the attempts. last_status_code and last_error (our transport error, never your body) give the cause. The run did not change.

You can replay a terminal delivery after you repair your receiver, or to do a test of a receiver. To replay, call POST /v1/format-runs/{run_id}/webhook/redeliver with formats:write and an empty body. It re-POSTs the current receipt with a new timestamp and signature. It does not use one of the ten automatic attempts. The call returns 409 webhook_not_configured when the run had no URL. It returns 409 run_not_terminal while the run is still in progress.

The full contract is on Run webhooks. That page also gives the generation-job webhooks that POST /v1/models/… emits on a different event set.

Watch a run progress

status tells you if a run is done. GET /v1/format-runs/{run_id}/events tells you what the run does now:

preparing is all the work before the agent gets the run. running is the time when the agent does the recipe, and most of the time goes there. finalizing is teardown and output harvest. Each entry has 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 become one entry, and its at continues to increase. Thus, at on the last entry is the progress clock of the run. If this clock does not move for several minutes, the run is stalled, not slow. Sume will finalize the run at the bounds above.

It is a phase timeline, not a log stream. Sume does not publish agent output, tool calls, or sandbox internals here, and will not publish them in the future. There is no push channel for progress. Poll this endpoint, or ignore it and use the terminal webhook.

The run receipt

GET /v1/format-runs/{run_id} returns the full shape at all statuses.

FieldNotes
id, objectarun_…, and format.run.
format{ id, slug, title, version }. id is the opaque skl_…. version is the Format version that ran. Later edits do not change it.
status, next_action, cancelableRefer to Lifecycle. cancelable is true while the run is queued or processing.
trigger{ source: "api", idempotency_key } for each run that you create.
created_at, started_at, finished_atThe last two are null until the events occur.
expires_at, queueThe deadline and the pickup state. Refer to Poll.
output_schema{ name, strict, source }: the schema that gave output its shape. source is request_override when you sent a schema.
outputThe structured result. null on each non-terminal status, and null when no result satisfied output_schema. A failed run still publishes the partial result that it wrote, if there is one.
output_error{ code, message, details } when the run could not produce output. Examine it before you read output.
primary_output_key, primary_output_urlThe one item to show. Both are null unless the run is completed.
artifacts[]Each durable file that the run generated: { id, type, url, content_type, size_bytes, width, height, duration_ms, checksum_sha256 }. Empty until the run is terminal. Also populated on failures.
usage{ currency, billable_amount_usd_micros, generation_spend_cap_usd_micros, debited_usd_micros, held_usd_micros, refunded_usd_micros, final, cap }. Refer to Reading usage. null when the API could not read the spend.
error{ code, message }. Non-null only when the run is failed.
skip_reasonSet on a skipped run.
webhook_deliveryThe delivery state for the URL that you registered, or null if you registered no URL.
idempotency_hittrue when this receipt is an idempotency replay, not a new run.
thread_id, previous_run_idThe conversation that this turn occurred in, and the run that it continued. Refer to Continue a run.
modelThe catalog id that the orchestrator ran on.
request_idLog it. Support asks for this value.
status_url, result_url, events_url, cancel_urlRefer to Poll.

Media URLs are durable media.sume.com HTTPS URLs. They do not expire. Each person who has the URL can open it. Thus, if your product needs per-customer access control, proxy or copy the media.

Continue a run

A Format run is one agent turn. If you send previous_run_id on a new POST …/runs, the next turn continues the same conversation. Sume replays to the agent what the agent produced. Thus, the agent can do one part again and keep the other parts as they are. Live-commerce integrations use this method to retry a single scene, and they do not pay for the full show again.

The run that you name must be continuable. Its receipt shows this: thread_id is not null, and the run completed or has a non-empty artifacts[]. You can continue a failed run that left work. You cannot continue a run that left nothing.

RefusalMeans
404 previous_run_not_foundThe id is unknown, or a different owner has the run.
400 previous_run_format_mismatchThat run started on a different Format. Continue it on the Format where it started.
409 previous_run_not_terminalThe run did not finish yet. Poll it. Then call again.
400 previous_run_not_resumableThere is nothing to continue: no thread_id, or the run is not completed and has no artifacts. details shows previous_run_status, has_thread, and artifact_count. Start a new run.

A continuation is a new run, with a new id, a new receipt, its own spend cap, and its own single webhook. The original run never changes. The two runs share thread_id, which is read-only. To continue, name previous_run_id, not a thread id (a thread id gives 400 unknown_parameter). Bind the same output_schema on each turn, because it is per run and not inherited. artifacts[] on a continued run lists all the media that the full conversation generated, but usage stays per run.

Cancel

The cancel call needs formats:write, and it is idempotent. In the two cases, the current receipt comes back, and cancel_effect tells you which case occurred. canceled means that this call stopped a run in progress. no_op means that the run already finished before the call. You pay for the generation that the run completed before the cancel, and usage shows it.

A canceled run never delivers a webhook. If your integration is webhook-only, cancel is the one path where no delivery will arrive. Use the receipt that this call returns.

List runs for a Format

The list shows the newest runs first. limit is 1–100, and the default is 20. GET /v1/formats/{format_id}/runs is the opaque twin. If no run of the Format ever started over the API, the call returns an empty list, not a 404.

The response is a page: send next_cursor back as cursor until has_more is false. The cursor is opaque and keyset over (created_at, id). Thus, runs created while you page do not move rows. A cursor that is not ours gives 400 invalid_request.

Routes that do not exist

Three paths that callers guess, and what to use in their place:

GuessUse
GET /v1/format-runsThere is no cross-Format list. List the runs of each Format, or keep your own index, with the data.id that you stored at create as the key.
GET /v1/formats/{handle}/{slug}/runs/{run_id}Read runs at /v1/format-runs/{run_id}. The Format path only creates and lists runs.
GET /v1/format-runs/{run_id}/messagesSume does not publish the conversation over the API. events_url gives the phase timeline. output and artifacts[] hold the result.

usage

usage.billable_amount_usd_micros is the generation spend of this run. Sume enforces the cap of the run against this total, and the total counts reserved and captured amounts. This value increases while the run is in progress, and settles when the run terminates. It does not include the LLM turn of the agent, thus it is not the total cost of the run. usage.cap gives the same calculation in parts: limit_usd_micros, counted_usd_micros (this value), and remaining_usd_micros.

The cost is usage.debited_usd_micros: the real amount that the wallet deducted for the run and its thread. This amount contains the captured ledger rows of all operation types, and also the LLM row of the turn. held_usd_micros are holds that are still open (not spend yet). refunded_usd_micros are holds that Sume gave back (not spend). final changes to true when no hold is open.

GET /v1/usage?run_id= uses the same rows and the same fold. Thus, the receipt, the ledger, and the answer of an agent always agree. usage is null when the API could not read the spend at all. This is different from 0. The three wallet fields are null on receipts written before the ledger answered.

Errors on the run endpoints

Statuserror.codeWhat to do
401unauthorizedThe key is missing, malformed, revoked, or unknown.
403insufficient_scopeThe key does not have formats:read (reads) or formats:write (cancel, redeliver). Mint a new key.
404format_run_not_foundThe run id is unknown, or a different owner has the run. A run that you cannot see gives the same result as a run that does not exist.
409run_not_completedYou sent GET …/result before the run was terminal. details.status holds the current status. Poll status_url. Then retry.
429rate_limitedWait for retry-after. Polls use the read budget. This budget is separate from the write budget, and much larger.
503studio_agent_upstream_unavailableAn outage on the Sume side, not a problem with your key. Retry at a later time. The run continues.

Errors and spend gives all the codes, together with the run-time failures (error and output_error).

Next