Jobs
Webhooks
Instead of polling, pass a URL and Sume POSTs the outcome there once the job ends. Every delivery is signed.
Set a webhook
Add mode: "webhook" and webhook_url to any generate request. The URL must be public HTTPS; localhost, private-network and plain HTTP URLs are refused when you submit.
cURL
curl -X POST https://api.dev.sume.com/v1/video-router/generate \
-H "Authorization: Bearer $SUME_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "seedance-2.5",
"prompt": "A paper boat drifting down a rain gutter",
"duration": 5,
"mode": "webhook",
"webhook_url": "https://example.com/hooks/sume"
}'Events
| event | status | When |
|---|---|---|
job.completed | OK | payload.artifacts holds the output. |
job.failed | ERROR | error says why. |
job.canceled | ERROR | The job was canceled. |
job.completed
{
"event": "job.completed",
"request_id": "job_7513b52d-3669-4ffe-b317-8b02000715eb",
"job_id": "job_7513b52d-3669-4ffe-b317-8b02000715eb",
"status": "OK",
"payload": {
"artifacts": [
{
"id": "artf_UCdnhWThU_Tbemv7E2gE7-ee9lNCIDxP",
"url": "https://media.sume.com/artifacts/artf_UCdnhWThU_Tbemv7E2gE7-ee9lNCIDxP/images.png",
"type": "image",
"content_type": "image/png"
}
]
}
}Verify the signature
Each delivery carries two headers:
Headers
x-sume-webhook-timestamp: 1792540000
x-sume-webhook-signature: sume-v1=5f2b…The signature is HMAC-SHA256, hex, over <timestamp>.<raw body> with your workspace's signing secret. Read the secret in Dashboard → Webhooks or from GET /v1/webhooks/signing-secret. While a secret rotates, the header holds one sume-v1= entry per live secret, comma-separated: accept the delivery if any entry matches. Refuse timestamps more than five minutes old.
import { createHmac, timingSafeEqual } from "node:crypto";
export function isSumeDelivery(rawBody, timestamp, signatureHeader, secret) {
const age = Math.abs(Date.now() / 1000 - Number(timestamp));
if (!Number.isFinite(age) || age > 300) return false;
const digest = createHmac("sha256", secret).update(`${timestamp}.${rawBody}`).digest("hex");
const expected = Buffer.from(`sume-v1=${digest}`);
return String(signatureHeader)
.split(",")
.map((entry) => Buffer.from(entry.trim()))
.some((entry) => entry.length === expected.length && timingSafeEqual(entry, expected));
}Verify the raw body exactly as received, before any JSON parsing.
Deliveries and retries
- Answer with any
2xxwithin 10 seconds, after you have stored the event. - A delivery without a
2xxis tried up to 10 times: 30 seconds after the first try, then twice as long each time, never more than an hour apart. - A delivery can arrive more than once; dedupe on
job_id. - The job's
webhook_deliveryfield records what was sent.POST /v1/jobs/{id}/webhook/redeliversends it again. - Keep a slow poll as a fallback in case your endpoint was down.