Ack a Sume webhook in 10 s, then copy the 30-second MP4 later
Sume gives each delivery 10 seconds. Verify, store the job id, answer 204, and let a worker download the MP4 instead of doing it inside the handler.

Do not download a finished 30-second video inside the webhook handler. Verify the signature, write the job_id to a durable queue, answer 204, and let a separate worker fetch the MP4. Sume gives each delivery attempt 10 seconds and retries slow endpoints, so a slow inline copy turns one delivery into several.
The 30-second models make this a real trap. A Seedance 2.5 or Wan 3.0 file is large, and you cannot promise it will finish downloading and uploading to storage within the budget.
What Sume does when you are slow
For job webhooks Sume retries network errors and non-2xx responses, up to 10 attempts total, with a fixed delay of 30 seconds by default and a 10-second timeout per attempt. A slow endpoint uses its budget and Sume retries it (Webhooks).
Ten attempts with nine 30-second gaps is at least four and a half minutes of spacing. If your handler is slow because it is busy copying files, each retry adds load at the worst moment, and the same job arrives again. job_id is the idempotency key, so you must dedupe anyway.
The handler that only acknowledges
This Node server verifies the sume-v1 signature, refuses to start without a secret, records the job id, answers 204, and leaves the copy to a worker. The queue here is an array only for the example; use a real queue or table.
import http from "node:http";
import crypto from "node:crypto";
const secret = process.env.SUME_COM_WEBHOOK_SIGNING_SECRET;
if (!secret) throw new Error("SUME_COM_WEBHOOK_SIGNING_SECRET is empty");
const queue = [];
http.createServer(async (req, res) => {
const chunks = [];
for await (const c of req) chunks.push(c);
const body = Buffer.concat(chunks).toString("utf8");
const ts = Number(req.headers["x-sume-webhook-timestamp"]);
const mac = crypto.createHmac("sha256", secret).update(`${ts}.${body}`).digest("hex");
const want = Buffer.from("sume-v1=" + mac);
const ok = Number.isFinite(ts) && Math.abs(Date.now() / 1000 - ts) <= 300 &&
String(req.headers["x-sume-webhook-signature"] || "").split(",")
.some((s) => { const b = Buffer.from(s.trim());
return b.length === want.length && crypto.timingSafeEqual(b, want); });
if (!ok) { res.statusCode = 401; return res.end(); }
const e = JSON.parse(body);
if (e.event === "job.completed") queue.push(e.job_id);
res.statusCode = 204; res.end();
}).listen(3000);What the worker does
The worker pops a job id, calls GET /v1/videos/{id}/content?index=0 with your API key, and writes the file to storage. If it fails, it retries later from the same id. Nothing here talks to Sume's webhook delivery, so a slow worker never causes a redelivery.
Because the event is only a notification, the worker should confirm GET /v1/jobs/{id}/status shows completed before it copies. For a failed or canceled event, record the outcome and stop.
Timing budget
| Item | Value | Consequence |
|---|---|---|
| Timeout per attempt | 10 s | Inline download of a 30 s MP4 may not fit |
| Attempts | 10 | A slow handler is retried |
| Spacing | 30 s fixed | At least 270 s across the nine gaps |
| Dedupe key | job_id | Safe to receive twice |
Why this order is safe
If the process dies after answering 204 but before the copy, you lose nothing: the job id is in your queue, or, if you lost the queue too, your reconcile poll will find the completed job. A webhook is a delivery optimization, not the only recovery path.
Checklist before you ship it
- The handler reads the raw body once and never parses it before the signature check.
- It answers within a second or two, far below the 10-second timeout.
- The
job_idis stored with a unique constraint so a retry is a no-op. - The worker owns downloads, uploads and any transcoding, with its own retry policy.
- A reconcile poll covers the case where every attempt failed.
Sources
Related posts
More in Developers
- Ad run returns 400 invalid_attachment: count your 30 files first
A Format run carries at most 30 files, split 30 images, 10 videos, 10 audio. A Python counter for attachments plus media URLs inside input, before you call.
- Agency Black Friday: a team-owned Format needs a workspace key
A personal API key cannot run a Format that a team workspace owns. Sume answers 403 workspace_key_required, so issue the key inside the team before launch.
- Get the edited image URL as a named field from an Agent Completion
Attach the photo and an output_schema in one POST /v1/agent/completions call. Sume parses output into your own fields, such as edited_url and changes.
- Agent Completion output_schema shaped like a Clef or Decider answer
Map Clef and Strands Decider answer types (yes/no, choice, score) onto a Sume output_schema with enum and integer bounds, and see what it does not check.
Written by Sume