Sume webhook signature header: why the sume-v1= prefix is checked
verifyWebhook only compares entries that start with sume-v1= and drops others, so a future scheme in the same header cannot break a receiver. A test proves it.

A Sume webhook receiver should treat x-sume-webhook-signature as a comma-separated list and only compare the entries that start with sume-v1=. That is exactly what verifyWebhook in @sume-com/sdk@0.2.0 does: it splits the header on commas, keeps the entries with the sume-v1= prefix, and drops the rest without failing. The SDK's source gives the reason: a future sume-v2= entry next to a sume-v1= entry must not make an old receiver fail, and that is the whole point of putting a version prefix on the wire.
The header already needs list handling today. During a signing-secret rotation Sume signs each delivery with both secrets for 24 hours and sends sume-v1=<new>,sume-v1=<old>, newest first (Verifying webhooks). A verifier that tests the whole header for equality fails on every delivery in that window.
What the verifier does with the header
The check has four rules worth knowing before you write your own. It reads the raw body and the timestamp header. It rejects a missing header, an empty secret or a non-numeric timestamp by returning false, never by throwing. It computes sume-v1= plus the hex HMAC-SHA256 of <timestamp>.<raw_body>. Then it compares every candidate entry in constant time, even after one matches, so timing does not reveal whether you hold the current secret or the one being rotated out.
Two more refusals matter. A header supplied twice, which Node exposes as an array, is ambiguous, so the verifier refuses it rather than picking one. And a delivery older than the replay window (300 seconds by default) fails, unless you set toleranceSeconds: 0, which turns that check off.
| Header value | Result |
|---|---|
| sume-v1=<correct> | true |
| sume-v1=<wrong>,sume-v1=<correct> | true |
| sume-v2=<anything>,sume-v1=<correct> | true, the v2 entry is dropped |
| sume-v2=<anything> only | false, no sume-v1 entry to compare |
| empty or missing header | false |
Prove it in a test
This script signs a body the way Sume does, with node:crypto, then verifies it with the SDK under three headers. It refuses to run without a secret value and uses a fixed clock through the now option, which exists as a test seam. Run it with Node 22 or later.
import crypto from "node:crypto";
import { verifyWebhook } from "@sume-com/sdk";
const secret = process.env.TEST_SECRET ?? "";
if (!secret) throw new Error("set TEST_SECRET to any non-empty string");
const body = JSON.stringify({ event: "job.completed", job_id: "job_test" });
const ts = 1780000000;
const hex = crypto.createHmac("sha256", secret).update(`${ts}.${body}`).digest("hex");
async function check(signature: string) {
return verifyWebhook({
body,
secret,
headers: {
"x-sume-webhook-timestamp": String(ts),
"x-sume-webhook-signature": signature,
},
now: () => ts,
});
}
console.log(await check(`sume-v1=${hex}`));
console.log(await check(`sume-v2=abc,sume-v1=${hex}`));
console.log(await check("sume-v2=abc"));Writing your own instead
If you cannot use the SDK, copy the shape, not just the HMAC. Split on commas, trim each entry, ignore anything without the sume-v1= prefix, and compare all remaining entries with a constant-time function such as crypto.timingSafeEqual on equal-length buffers. The webhooks page has a TypeScript version of this, and the same scheme covers run webhooks, so one verifier handles both.
Related posts
More in Developers
- v1/videos/models `created` is a catalog date, not a release date
Every model on Sume's /v1/videos/models shows created 1767225600, which is 2026-01-01. It is not when Gemini Omni 1.1 Flash or MiniMax H3 launched.
- Voice agent hand-off: ask for a clip, get an async Sume job
A live voice agent should not wait on a video render. Hand the request to an async Sume job, speak the job id back, and deliver the clip by poll or webhook.
- VS Code 1.140 shared MCP config files: what goes in the Sume entry
VS Code 1.140 lets MCP servers live in portable config files shared across Copilot tools. For Sume the entry is one URL, and no key belongs in the file.
- waitForJob throws on a failed poll: resume by job id in TypeScript
Unlike waitForRun, waitForJob has no transient-failure budget: one failed status read after the client's retries throws. Wrap it and resume by job id.
Written by Sume