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
| Environment | Delivery |
|---|---|
Development — api.dev.sume.com | Live. Sume calls your endpoint. |
Production — api.sume.com | Live. 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
| Field | Notes |
|---|---|
communication.webhook_url | Public HTTPS URL, maximum 2048 characters. Sume rejects localhost, private-network, and non-HTTPS URLs with 400 invalid_request. |
communication.callback_url | Accepted alias for webhook_url. The behavior is the same. Send one or the other. |
communication.mode | async (default) or webhook. The URL arms delivery. mode is only descriptive. |
Top-level webhook_url / callback_url / mode | fal-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.
| Surface | Event | Receipt object |
|---|---|---|
| Action runs | action.run.terminal | action.run |
| Format runs | format.run.terminal | format.run |
| Agent Completions | agent.run.terminal | agent.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
| Field | Notes |
|---|---|
event | Refer to the table above. |
request_id | Equals run_id. Stable across retries. Use it to dedupe. |
run_id | The run that this delivery is about. |
object | The object of the receipt. |
status | OK when the run completed, ERROR when it failed. Binary. Refer to outcome. |
outcome | ok, degraded, or error. Branch on this when the question is "did I get usable output". |
created_at | The 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. |
payload | The run receipt. null only on overflow. Refer to the section below. |
error | null 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
| Property | Value |
|---|---|
| When | One time for each run, when the run completes or fails. |
| Success | Any 2xx. |
| Retries | A maximum of 10 attempts in total. Then webhook_delivery.status is exhausted. |
| Backoff | min(max(30s × 2^(attempt−1) with jitter, Retry-After), 1h). Obey Retry-After on 429/503. |
| Timeout | 10s per attempt. |
| Redirects | Sume 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
- Verifying webhooks —
verifyWebhookandSUME_COM_WEBHOOK_SIGNING_SECRET - Runs and results — the receipt, field by field
- Advanced: run a schedule via API
- Calling a Format
- Embed a Format in your product — the whole partner integration, end to end
- Webhooks — generation-job webhooks, the other surface