Developers

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.

6 min readSume
All posts

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_url is an accepted alias. communication.mode is async (the default) or webhook, but it is descriptive: the URL is what arms delivery.
  • Sume re-validates the URL at delivery time. Redirects are not followed, so a 3xx is not a delivery.
  • Generation jobs from model endpoints such as /v1/avatar-1.0/generate take mode: "webhook" with webhook_url and send job.* 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.

Event names from Run webhooks and Webhooks, read 2026-09-25.
You calledEventDedupe on
A Format runformat.run.terminalrequest_id, which equals run_id
An Action runaction.run.terminalrequest_id, which equals run_id
An Agent Completionagent.run.terminalrequest_id, which equals run_id
A model endpoint jobjob.completed, job.failed, job.canceledjob_id

What is in a run webhook payload?

The envelope wraps the run receipt:

  • status is OK when the run completed and ERROR when it failed. outcome is ok, degraded, or error; branch on it when the question is whether you got usable output.
  • degraded means the run completed and made real media in artifacts[], but could not project it into your output_schema, so output is null.
  • request_id is stable across retries, so it is the dedupe key. Use created_at to order deliveries.
  • payload is byte-identical to the data object of GET /v1/{family}-runs/{run_id}, so one handler serves webhook and poll.
  • A receipt over 1 MiB arrives with payload: null and error code payload_too_large. Fetch it from result_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.
  • verifyWebhook is async, returns false rather than throwing, compares in constant time, and enforces a replay window, toleranceSeconds, that defaults to 300.
  • Read the signing secret on the dashboard Webhooks tab or from GET /v1/webhooks/signing-secret with a key carrying account:read. It is derived for your workspace. Store it as SUME_COM_WEBHOOK_SIGNING_SECRET.
  • If a signature will not verify, compare the x-sume-webhook-secret-fingerprint header 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 with 204.
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 dummy webhook.test payload. It is not a replay of a real run.
Delivery rules from Run webhooks and Webhooks, read 2026-09-25.
PropertyRun webhooksJob webhooks
AttemptsUp to 10 attempts total, then exhaustedUp to 10 attempts total
Spacingmin(max(30s × 2^(attempt−1) with jitter, Retry-After), 1h)A fixed delay, 30s by default, not exponential backoff
Timeout10s per attempt10s per attempt
RedeliverFormat runs: POST /v1/format-runs/{run_id}/webhook/redeliver with formats:writePOST /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

Written by Sume