Verify a Sume avatar video webhook in Node and TypeScript
A Node server that refuses an empty secret, checks the x-sume-webhook-signature HMAC over timestamp.rawBody, then fetches the avatar video result by job_id.
To verify a Sume avatar video webhook in Node, read the raw body before parsing it, compute HMAC SHA-256 over {timestamp}.{raw_body} with your signing secret, and compare it to the sume-v1= entries in x-sume-webhook-signature using a constant-time check. Refuse to start if the secret is empty, and reject timestamps older than your replay window.
The signature scheme is the same for every Sume job webhook, so this one verifier also covers run webhooks.
What does Sume send for an avatar video?
Submit POST /v1/avatar-1.0/talking-video with mode: "webhook" and a public HTTPS webhook_url. Localhost, private-network and non-HTTPS URLs are rejected. Sume sends terminal events only: job.completed, job.failed and job.canceled. There are no progress deliveries.
The body carries event, request_id, job_id and status, plus a payload. Treat job_id as your idempotency key, because the same event can arrive more than once.
What does the headers check look like?
Two headers matter: x-sume-webhook-timestamp and x-sume-webhook-signature: sume-v1=<hex>. During a signing-secret rotation the signature header carries one entry per live secret, newest first, separated by commas, so accept the delivery if any entry matches.
Read the secret from the dashboard Webhooks tab or GET /v1/webhooks/signing-secret with a key carrying account:read, and store it as SUME_COM_WEBHOOK_SIGNING_SECRET.
What is a working handler?
This is a complete server using only Node built-ins. It verifies against the raw string, returns 401 on a bad signature and 200 after you would have stored the event.
import crypto from "node:crypto";
import http from "node:http";
const secret = process.env.SUME_COM_WEBHOOK_SIGNING_SECRET ?? "";
if (!secret) throw new Error("SUME_COM_WEBHOOK_SIGNING_SECRET is empty");
function verify(raw: string, ts: string, header: string): boolean {
const n = Number(ts);
if (!Number.isFinite(n) || Math.abs(Date.now() / 1000 - n) > 300) return false;
const sig = crypto.createHmac("sha256", secret).update(`${n}.${raw}`).digest("hex");
const want = Buffer.from(`sume-v1=${sig}`);
return header.split(",").some((e) => {
const got = Buffer.from(e.trim());
return got.length === want.length && crypto.timingSafeEqual(got, want);
});
}
http.createServer((req, res) => {
const chunks: Buffer[] = [];
req.on("data", (c: Buffer) => chunks.push(c));
req.on("end", () => {
const raw = Buffer.concat(chunks).toString("utf8");
const ts = String(req.headers["x-sume-webhook-timestamp"] ?? "");
const sig = String(req.headers["x-sume-webhook-signature"] ?? "");
if (!verify(raw, ts, sig)) { res.statusCode = 401; return res.end(); }
const evt = JSON.parse(raw);
console.log(evt.event, evt.job_id);
res.statusCode = 200;
res.end("ok");
});
}).listen(3000);What should I do after it verifies?
Store the event durably, return any 2xx, then fetch the result for the job_id with GET /v1/jobs/{job_id}/result rather than trusting the callback body alone. A webhook is a delivery optimisation, not your only recovery path: keep status_url polling for deliveries that never arrive.
What about retries and test sends?
Sume makes up to 10 attempts in total, with a fixed delay between attempts (30 seconds by default) and a 10-second timeout per attempt. A slow endpoint burns the budget. After exhaustion the job still reached its real terminal state, and POST /v1/jobs/{job_id}/webhook/redeliver re-sends the real terminal event with a fresh timestamp and signature without using one of the automatic attempts.
Send test (POST /v1/webhooks/test-deliveries) posts a dummy webhook.test payload with no job_id; do not treat it as a real job. If a signature will not verify, compare x-sume-webhook-secret-fingerprint with the fingerprint beside the secret in the dashboard. For the Python version of this verifier, see verify a Sume avatar video webhook in Python.
Sources
Related posts
More in Developers
- low_confidence_long_video: why video_frames warns past 90 seconds
Sume's video_frames returns the low_confidence_long_video warning when the source runs over 90 s. The job still succeeds; the hard cap is 300 s. What to do.
- Check has_audio first: video_inspect frames false before STT or detach
A free probe-only video_inspect tells you probe.has_audio before you reserve STT or run audio detach, so silent clips never hit the no-audio errors.
- video_inspect silence_split_seconds: sentence segments for captions
How silence_split_seconds (0.2 to 3) shapes Sume video-inspect sentence segments, the 0.5 s default in the repo, and turning segments into caption cues.
- Voice API deadlines, October 2026 to February 2027
A calendar of voice and transcription API changes from vendor pages: Gemini TTS price rise, OpenAI transcription shutdown, and the xAI voice alias move.
Written by Sume