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.
- Ruby Net::HTTP: POST /v1/videos with callback_url, for a Sora port
A 15-line Net::HTTP submit for /v1/videos that sends an HTTPS callback_url and an Idempotency-Key, checks for 202, and stores the id before any callback.
- Ruby Net::HTTP: submit a Wan 3.0 video job and poll until it finishes
A standard-library Ruby script that posts to /v1/videos with wan-3.0, polls the polling_url every 30 seconds and stops on completed, failed or cancelled.
- Ruby Thread and Queue: four workers submit Wan 3.0 clips
Ruby worker pool for POST /v1/videos: a Queue of shots, four Threads, Net::HTTP and one Idempotency-Key per shot. Tested on Ruby with a stub server.
Written by Sume