Run webhooks

Each run surface accepts a communication.webhook_url. When the run completes or fails, Sume sends one signed POST to that URL. The POST contains the same receipt that the poll endpoints return. The delivery uses the same shape as the fal webhook. The envelope status is OK or ERROR for those outcomes, but not for cancel.

A webhook is the alternative to a poll loop. You still get status_url and result_url, and you can still poll. A webhook only means that you do not have to run a loop for each run.

Availability

EnvironmentDelivery
Development — api.dev.sume.comLive. Sume calls your endpoint.
Production — api.sume.comLive. Sume calls your endpoint.

If you supply communication.webhook_url, Sume arms delivery in both environments. You can still poll status_url / result_url as a backup. Any run whose caller already supplied a webhook_url can receive a POST. This includes older runs that get to a terminal state after enablement. Register only the endpoints where you still want traffic.

This page is about run webhooks. Generation-job webhooks (job.completed and related events, from POST /v1/models/...) are a separate surface with a separate event set. Refer to Webhooks. The signature scheme is the same, so one verifier works for both.

Ask for a webhook

When you start the run, send communication.webhook_url. It works the same on all three surfaces.

Create a Format run

POST /v1/formats/{handle}/{slug}/runs

Required

FieldNotes
communication.webhook_urlPublic HTTPS URL, maximum 2048 characters. Sume rejects localhost, private-network, and non-HTTPS URLs with 400 invalid_request.
communication.callback_urlAccepted alias for webhook_url. The behavior is the same. Send one or the other.
communication.modeasync (default) or webhook. The URL arms delivery. mode is only descriptive.
Top-level webhook_url / callback_url / modefal-shaped aliases. Sume normalizes them into communication.*. The same value on both layers is permitted. Values that do not agree return 400 invalid_request.

Sume validates the URL as a public HTTPS URL again at delivery time, not only when you submit. Sume does not follow redirects. A 3xx is not a delivery.

Events

Each run family has one terminal event. The outcome is in status and payload.status, not in the event name.

SurfaceEventReceipt object
Action runsaction.run.terminalaction.run
Format runsformat.run.terminalformat.run
Agent Completionsagent.run.terminalagent.run

A partner that embeds only Formats can route on event === "format.run.terminal" and does not have to examine the body.

One per turn, not one per artifact

A run is one agent turn. Thus, its terminal event fires exactly one time. The number of clips, images, or intermediate files that the turn produced does not change this. There is no per-artifact run event, and we do not plan one.

This fact is important for continued runs. When you continue a run, Sume starts a new run. The new run delivers its own single terminal webhook with the new run id. The webhook of the original run already fired and will not fire again.

If you want progress inside a turn, use the generation-job layer (Webhooks). That layer fires one event for each job when the job completes. Job events name a job, not a step of your recipe.

Payload

FieldNotes
eventRefer to the table above.
request_idEquals run_id. Stable across retries. Use it to dedupe.
run_idThe run that this delivery is about.
objectThe object of the receipt.
statusOK when the run completed, ERROR when it failed. Binary. Refer to outcome.
outcomeok, degraded, or error. Branch on this when the question is "did I get usable output".
created_atThe time when Sume built this delivery body. Use it to put deliveries in order. request_id cannot do this, because it is stable across retries.
payloadThe run receipt. null only on overflow. Refer to the section below.
errornull when status is OK. In other cases, { code, message }.

does not always mean you got output

A run can complete, bill you, and produce real media in artifacts[], but still fail to project that media into your output_schema. On that path, status is OK, because the run really completed. But output is null, and output_error gives the reason. outcome: "degraded" is the name for this condition.

A handler that uses only status continues to work. But it cannot tell the difference between ok and degraded.

request_id

The request_id of the envelope is the run id. It is the dedupe key, and it is deliberately stable across retries.

The receipt value at payload.request_id is a correlation id for the call that produced the receipt. In the webhook, this value is the run id. When you read the same receipt from GET /v1/format-runs/{run_id}, this value is an HTTP req_… id. Dedupe on the request_id of the envelope (or run_id, which equals it). Ignore the nested value.

usage.billable_amount_usd_micros contains the generation spend that Sume attributes to the run. If Sume cannot read that spend, usage is null. Refer to Runs and results. GET /v1/usage stays the authoritative billing record.

is the receipt

payload is byte-identical to the data object of GET /v1/{family}-runs/{run_id} for the same run. The poll response wraps it in { "data": ... }. The webhook does not wrap it.

The same code path that the poll endpoint calls builds the payload. Thus, the two cannot become different.

Failure, cancelation, and skips

A failed run comes with status: "ERROR" and a populated error. payload is still the full receipt. The receipt of a failed run contains artifacts and output_error, and usually you want them.

error.code is a copy of payload.error.code when the receipt has one. Usually it is a specific reason such as output_schema_unsatisfied. If not, it is the generic code of the family: action_run_failed / format_run_failed / agent_run_failed.

A canceled run does not deliver a webhook. Cancel is a separate API path (the same idea as the fal queue cancel, with no cancel webhook status). After you POST …/cancel, accept the cancel response as correct. Then poll status_url until payload.status is canceled. Do not wait for a POST.

A skipped run does not deliver a webhook. on_active_run: "skip" records a terminal run immediately and does not start work. Thus, there is no completion to tell you about. The create response already told you. Read status on the response that you got. Do not wait for a POST that will not arrive.

The defaults are different for each surface. The Format default is allow (concurrency). The Action default is skip. For Formats, refer to Calling a Format. For Actions, refer to Action API trigger.

Oversized receipts

Sume cannot deliver a receipt of more than 1 MiB inline. Sume sends the envelope with payload: null and an error that tells you where to fetch the receipt:

status still reports the real outcome of the run. If a run succeeded but was too large to ship, the run did not fail.

Signature

Sume uses HMAC-SHA256 over <timestamp>.<raw_body> to sign the raw JSON body.

Verify the signature against the raw request body, before any JSON parse or re-serialize. Reject a timestamp that is outside your replay window. Five minutes is a good default.

You can read your signing secret on the Webhooks tab of the dashboard (/dashboard/webhooks). You can also get it from GET /v1/webhooks/signing-secret with any API key that has account:read. Sume derives the secret for your workspace. The secret of a different workspace cannot verify a delivery that Sume signed for you. Store the secret as SUME_COM_WEBHOOK_SIGNING_SECRET (the same name that the delivery worker uses when it signs).

Each delivery has x-sume-webhook-secret-fingerprint. webhook_delivery.signing_secret_fingerprint on the run receipt repeats this value. Compare it with the fingerprint next to the secret in the dashboard. Then you know that both sides have the same secret, and you do not send the secret anywhere.

In TypeScript, @sume-com/sdk ships this check (refer to Verifying webhooks):

This is the full scheme. Use it in a receiver that cannot use the SDK, for example another language or a gateway in front of your app:

This is the same verifier that validates generation-job webhooks. Write it once.

Delivery behavior

PropertyValue
WhenOne time for each run, when the run completes or fails.
SuccessAny 2xx.
RetriesA maximum of 10 attempts in total. Then webhook_delivery.status is exhausted.
Backoffmin(max(30s × 2^(attempt−1) with jitter, Retry-After), 1h). Obey Retry-After on 429/503.
Timeout10s per attempt.
RedirectsSume does not follow them. A 3xx is a failed attempt.

First, record the event durably. Then return 2xx quickly. Process the event after you return the response. A slow endpoint uses all of the 10-second attempt budget, and Sume retries the delivery.

Dedupe on request_id. It is the same value on each retry of the same run.

A delivery outcome does not change the run. If an endpoint rejects all ten attempts, you have a failed delivery and a run that is still completed. Fetch the run from result_url.

Send test and Redeliver

There is one Send test control, on /dashboard/webhooks (also POST /v1/webhooks/test-deliveries, account:write). It sends a dummy webhook.test payload. It is not a replay of a real Format run.

To replay a real terminal call, use Redeliver on that delivery row, or:

The key must have formats:write. The body is empty. The endpoint re-POSTs the current format.run.terminal receipt with a new timestamp and signature. Sume signs it with the same secret as the original delivery. Thus, x-sume-webhook-secret-fingerprint (and webhook_delivery.signing_secret_fingerprint on the row) is the same 12 characters, and your verifier does not change.

This still works after the automatic attempts are exhausted. It does not use one of the automatic 10.

The response has redelivery (delivered, status_code, error) for the POST that you triggered. webhook_delivery describes the row. If a redeliver fails for a call that Sume delivered before, the row keeps that earlier 2xx. Sume only counts the attempt in manual_redeliveries.

If the run had no webhook_url, the endpoint returns 409 webhook_not_configured. If the run is still in progress, it returns 409 run_not_terminal. A run that you cannot see returns 404 format_run_not_found. If the key does not have formats:write, the result is 403 insufficient_scope, not 404.

Dedupe on request_id / run_id. Redeliver does not send to a different URL.

Webhooks documents job redeliver.

Next