Route Sume run webhooks by event: format, action and agent terminal
Run webhooks use one terminal event per family: action.run.terminal, format.run.terminal, agent.run.terminal. Route on event, then branch on outcome.

A run webhook has exactly one terminal event for each run family, and the event name never carries the outcome. Use action.run.terminal for Action runs, format.run.terminal for Format runs and agent.run.terminal for Agent Completions. Route on event, then read status and outcome to learn what happened. A service that embeds only Formats can match event === "format.run.terminal" and ignore every other delivery.
The three events
| 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 |
Generation-job webhooks are a different surface. Their events start with job., such as job.completed, and have their own event set. The signature scheme is the same, so one verifier covers both, but your router needs a separate branch for the job.* names.
Branch on outcome, not on the name
On a finished run, status is OK or ERROR, and outcome refines it. ok means the run completed with output. degraded means the run completed and was 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 means the run did not complete. Cancel is not in this list: a canceled run never delivers a webhook.
| `outcome` | `status` | Usable media |
|---|---|---|
ok | OK | Yes, output is filled |
degraded | OK | Yes, in artifacts[]; output is null |
error | ERROR | No. See error.code |
A small router
The function maps the event to a family and reports whether media is usable. It performs no verification, so call your signature check first, on the raw body. It returns unknown for events it does not know, which lets you acknowledge and ignore new event names without failing the delivery.
// One terminal event per run family. The outcome lives in status and outcome.
const FAMILIES = {
"action.run.terminal": "action",
"format.run.terminal": "format",
"agent.run.terminal": "agent",
};
export function route(envelope) {
const family = FAMILIES[envelope.event];
if (!family) return { family: "unknown" };
const usable = envelope.outcome === "ok" || envelope.outcome === "degraded";
return { family, runId: envelope.run_id, usable, partial: envelope.outcome === "degraded" };
}
console.log(route({ event: "format.run.terminal", run_id: "arun_demo", status: "OK", outcome: "degraded" }));
console.log(route({ event: "job.completed" }));Handler order
Do the work in this order. Read the raw body and verify the signature, rejecting timestamps outside the five-minute window. Parse the JSON. Look up run_id in your own table to skip duplicates. Route on event. Then branch on outcome. Acknowledge quickly: each attempt has a 10 second timeout, and ten failed attempts end in failed or exhausted on the receipt's webhook_delivery. Do the slow work, such as downloading media, after you have answered.
Other fields to rely on
run_idandrequest_idare equal and stable across retries. Use either as your dedupe key.created_atis when Sume built this delivery body, so use it, notrequest_id, to order deliveries.payloadis byte-identical todatafrom the receipt route, so one parser can serve a webhook and a poll.payloadisnullwhen the receipt was over 1 MiB. Thenerror.codeispayload_too_largeanderror.result_urlis where to fetch it.statusstill shows the real outcome.- A continued run is a new run with a new id, and it delivers its own single terminal event. The original run's webhook does not fire again.
One event per run, not per clip
A run is one agent turn, so its terminal event fires once, whatever the number of clips or images it produced. There is no per-artifact event. For progress inside a turn, use the generation-job layer, which fires one event per job on completion. See Run webhooks for the payload and Webhooks for the job events.
Sources
Related posts
More in Developers
- Ruby Net::HTTP: create a Sume bulk queue and poll to an exit code
A 30-line Ruby script with only the standard library: create a Sume bulk queue from items.json, back off the poll, skip 429 and 503, and exit 1 on failed items.
- SHA-256 the batch body into the Sume idempotency key for bulk chunks
Derive each bulk chunk's Idempotency-Key from a hash of its items so replays reuse the queue and edited rows get a new key instead of a 409 conflict.
- Should my backend call Sume over hosted MCP or the REST API?
REST from a backend, hosted MCP from an agent client. Where they differ: auth, wait limits, REST-only Image 1.0 and Video 1.0, and write budgets.
- Retiring a webhook endpoint: Sume runs already carrying it still POST
Any run created with a webhook_url can POST when it ends, even after you decommission the endpoint. Retries run 10 times, and the receipt holds the real result.
Written by Sume