One handler for both: Sume run webhook payload equals the GET receipt
A Sume run webhook's payload is byte-identical to GET /v1/format-runs/{run_id}. Write one function for polling and webhook, and branch on outcome.

If you poll a Sume Format run and also take a webhook for it, you can write the code that reads a finished run once. The run webhook envelope carries the whole run receipt in its payload field, and the docs say that receipt is byte-identical to data from GET /v1/format-runs/{run_id}. One handler serves both transports.
What is in the envelope
A delivery for the event format.run.terminal has event, request_id, run_id, object, status, outcome, created_at, payload and error. request_id equals the run id and is stable across retries, so use it to dedupe. created_at is the time Sume built that body, so use it to order deliveries.
status is binary: OK when the run completed, ERROR when it failed. outcome is the field to branch on, with the values ok, degraded and error. A completed run whose output could not be projected into your output_schema is OK with outcome degraded, an output of null and an output_error that gives the reason.
One function for both paths
Write a function that takes the receipt, the payload, and decides what to do. A poller calls it with data from the GET. A webhook handler calls it with payload from the verified body. The test suite then needs one set of fixtures, and a fix lands in both paths.
function handleReceipt(run, outcome) {
if (outcome === "ok") return { ship: run.output, url: run.primary_output_url };
if (outcome === "degraded") return { review: run.artifacts, why: run.output_error };
return { failed: run.error };
}
export function fromPoll(body) {
const run = body.data ?? body;
const outcome = run.status === "completed" ? (run.output ? "ok" : "degraded") : "error";
return handleReceipt(run, outcome);
}
export function fromWebhook(event) {
if (event.payload === null) return { refetch: event.error?.result_url };
return handleReceipt(event.payload, event.outcome);
}Two cases that differ
The first is overflow. If the receipt is larger than 1 MiB, payload is null and error.code is payload_too_large, with a result_url to fetch the receipt. The handler above refetches in that case.
The second is outcome. A poll gives you no outcome field, so compute it from status and output as the sample does. Verify the signature of the delivery against the raw body before you trust any of it; the SDK function verifyWebhook is async and returns false instead of throwing.
Sources
Related posts
More in Developers
- One Sume probe, eight platform limits: a Python preflight
Check one video inspect probe against Truth Social, Odysee, ArtStation, Linktree, Spotlight, Reels, TikTok API and Shorts limits in under 30 lines of Python.
- Agents SDK blocked_tool_names: hide Sume paid tools from a model
create_static_tool_filter takes allowed and blocked tool names. Use it so an agent on Sume's hosted MCP can read, but never sees a paid generate tool.
- OpenAI gave 184 days to leave the Videos API: a CI catalog check
Notice March 24, removal September 24, 2026, no replacement listed. An 18-line Python check fails CI when a video model id or duration leaves Sume's catalog.
- OpenRouter puts an idempotency key on video webhooks; Sume uses job_id
OpenRouter's video guide adds an idempotency key header to each delivery. Sume dedupes on job_id and takes Idempotency-Key on submit. Which key goes where.
Written by Sume