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.

5 min readSume
All posts

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

Delivery budget vs inline copy, Sume webhook docs read 2026-10-05
ItemValueConsequence
Timeout per attempt10 sInline download of a 30 s MP4 may not fit
Attempts10A slow handler is retried
Spacing30 s fixedAt least 270 s across the nine gaps
Dedupe keyjob_idSafe 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_id is 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

All Developers posts

Written by Sume