Signed webhooks for Sume video runs: events, retries, verification
Sume sends one HMAC-SHA256 signed POST when a Format, Action, or Agent Completion run completes or fails. Verify the raw body and dedupe on request_id.

A Sume run webhook is one signed POST that Sume sends to your communication.webhook_url when an Action, Format, or Agent Completion run completes or fails, carrying the same receipt the poll endpoints return. It is signed with HMAC-SHA256 over <timestamp>.<raw_body>, so your receiver verifies the raw body, dedupes on request_id, and returns a 2xx quickly.
The rules below come from Sume's Run webhooks, generation-job Webhooks, and SDK Verifying webhooks pages. Delivery is live on production, api.sume.com, and polling stays supported as a backup.
How do I ask for a webhook?
Send communication.webhook_url when you start the run. It works the same on all three run surfaces.
- The URL must be public HTTPS, at most 2048 characters. Localhost, private-network, and non-HTTPS URLs are rejected with
400 invalid_request. communication.callback_urlis an accepted alias.communication.modeisasync(the default) orwebhook, but it is descriptive: the URL is what arms delivery.- Sume re-validates the URL at delivery time. Redirects are not followed, so a
3xxis not a delivery. - Generation jobs from model endpoints such as
/v1/avatar-1.0/generatetakemode: "webhook"withwebhook_urland sendjob.*events instead.
Which events will my endpoint receive?
Terminal events only. A run is one agent turn, so its event fires exactly once however many clips or images the turn produced, and continuing a run starts a new run with its own webhook. The outcome lives in status and payload.status, not in the event name. Format run lifecycle covers the receipt itself.
| You called | Event | Dedupe on |
|---|---|---|
| A Format run | format.run.terminal | request_id, which equals run_id |
| An Action run | action.run.terminal | request_id, which equals run_id |
| An Agent Completion | agent.run.terminal | request_id, which equals run_id |
| A model endpoint job | job.completed, job.failed, job.canceled | job_id |
What is in a run webhook payload?
The envelope wraps the run receipt:
statusisOKwhen the run completed andERRORwhen it failed.outcomeisok,degraded, orerror; branch on it when the question is whether you got usable output.degradedmeans the run completed and made real media inartifacts[], but could not project it into youroutput_schema, sooutputisnull.request_idis stable across retries, so it is the dedupe key. Usecreated_atto order deliveries.payloadis byte-identical to thedataobject ofGET /v1/{family}-runs/{run_id}, so one handler serves webhook and poll.- A receipt over 1 MiB arrives with
payload: nulland error codepayload_too_large. Fetch it fromresult_url. - Canceled and skipped runs never deliver a run webhook. Trust the cancel response or the create response instead.
How do I verify the webhook signature?
Each delivery carries x-sume-webhook-timestamp and x-sume-webhook-signature: sume-v1=<hex_signature>. In TypeScript, verifyWebhook from @sume-com/sdk runs the check, as in the handler below. What matters:
- Pass the raw body. A parsed and re-serialized object does not verify, because key order and whitespace are part of what was signed.
verifyWebhookisasync, returnsfalserather than throwing, compares in constant time, and enforces a replay window,toleranceSeconds, that defaults to300.- Read the signing secret on the dashboard Webhooks tab or from
GET /v1/webhooks/signing-secretwith a key carryingaccount:read. It is derived for your workspace. Store it asSUME_COM_WEBHOOK_SIGNING_SECRET. - If a signature will not verify, compare the
x-sume-webhook-secret-fingerprintheader with the fingerprint in the dashboard. - Run and job webhooks share one secret and one scheme, so one verifier covers both. Route on
event, and answer unknown events with204.
import { verifyWebhook } from "@sume-com/sdk";
export async function POST(request: Request) {
const body = await request.text(); // raw, before any JSON.parse
const ok = await verifyWebhook({
body,
headers: request.headers,
secret: process.env.SUME_COM_WEBHOOK_SIGNING_SECRET!,
});
if (!ok) return new Response("bad signature", { status: 401 });
const event = JSON.parse(body);
await recordTerminalRun(event.request_id, event); // dedupe on request_id
return new Response(null, { status: 204 }); // fast 2xx, then work
}What happens if my endpoint is down?
Return any 2xx quickly, after durably recording the event, and process afterward. A delivery outcome never changes the run itself; if every attempt is refused, read the run from result_url.
- Redeliver re-POSTs the real terminal event with a fresh timestamp and signature. It still works after automatic attempts are exhausted and does not consume one of the automatic 10.
- Send test (
POST /v1/webhooks/test-deliveries,account:write) fires a dummywebhook.testpayload. It is not a replay of a real run.
| Property | Run webhooks | Job webhooks |
|---|---|---|
| Attempts | Up to 10 attempts total, then exhausted | Up to 10 attempts total |
| Spacing | min(max(30s × 2^(attempt−1) with jitter, Retry-After), 1h) | A fixed delay, 30s by default, not exponential backoff |
| Timeout | 10s per attempt | 10s per attempt |
| Redeliver | Format runs: POST /v1/format-runs/{run_id}/webhook/redeliver with formats:write | POST /v1/jobs/{job_id}/webhook/redeliver with jobs:write |
How do I rotate the webhook signing secret?
Use Webhooks → Rotate secret, or POST /v1/webhooks/signing-secret/rotate with a key carrying account:write. Rotation is not a cutover: for 24 hours afterwards, Sume signs every delivery with both secrets and sends them comma-separated in x-sume-webhook-signature, newest first. Accept a delivery when any sume-v1= entry matches.
verifyWebhook in @sume-com/sdk 0.2.0 already handles the multi-signature header. A hand-rolled verifier that compares the header for equality fails on every delivery during the window, so upgrade the receiver before you rotate.
Sources
Related posts
More in Developers
- Spend caps for unattended AI agents: how Sume bounds each run
An unattended agent has no one to approve spend, so Sume caps generation per run: required on Agent Completions, and up to $500 on Format runs.
- Sume API errors and rate limits: codes, 429s, and when to retry
Sume API errors share one envelope with a stable code and a request id. Reads and writes have separate per-minute budgets; queue_full is not a rate limit.
- How Sume API keys work: scopes, auth headers, hosts, and rotation
A Sume API key is a workspace-scoped secret sent as Bearer or x-api-key, never both. Scopes are fixed at creation, and a key works only on its own host.
- How to generate avatar video from the terminal with the Sume CLI
Install the Sume CLI, sign in through the browser, and submit talking avatar videos. Image, Video, and Music 1.0 have no CLI submit command yet.
Written by Sume