verifyWebhook returns false during a Sume secret rotation
The npm build of @sume-com/sdk 0.2.0 compares the signature header for equality, so rotation deliveries with two signatures fail. A 21-line fix.

If you installed @sume-com/sdk@0.2.0 from npm and call verifyWebhook, it will return false for every delivery during the 24 hours after you rotate your webhook signing secret. During that window Sume sends two signatures in one header, and the npm build compares the whole header to a single expected value. A verifier that checks each comma-separated entry, like the 21-line one below, passes both.
The Sume docs say verifyWebhook in 0.2.0 already handles the multi-signature header, and the source in the repository does split on commas. The registry tells a different story: npm lists 0.2.0 as published on 2026-08-02, and the code in that tarball does an exact string compare. I ran both against the same inputs on 2026-10-02, so treat the tarball as the thing that runs in your build.
What I checked
I installed the package from npm, signed a body with a test secret using the documented scheme (HMAC-SHA256 over timestamp.rawbody), and called verifyWebhook with a header of the form sume-v1=<new>,sume-v1=<old>. The check is a plain string compare, so the runtime does not change the result.
| Header sent | npm 0.2.0 | Verifier below |
|---|---|---|
| One valid signature | true | true |
| New and old secret, your secret second | false | true |
| Your secret first, old second | false | true |
| Signature from a different secret | false | false |
| Empty secret | false | false |
A verifier that accepts rotation
Pass the raw body string, not a parsed object, and the request Headers. It uses WebCrypto only, so it runs unchanged on Node 20+, Bun, Deno and Workers, and it refuses an empty secret.
const enc = new TextEncoder();
export async function verifySume({ body, headers, secret, tolerance = 300 }) {
if (!secret) return false; // never verify against an empty secret
const ts = Number(headers.get("x-sume-webhook-timestamp"));
const header = headers.get("x-sume-webhook-signature") ?? "";
if (!Number.isFinite(ts) || !header) return false;
if (Math.abs(Date.now() / 1000 - ts) > tolerance) return false;
const key = await crypto.subtle.importKey(
"raw", enc.encode(secret), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
const mac = new Uint8Array(await crypto.subtle.sign("HMAC", key, enc.encode(`${ts}.${body}`)));
const expected = "sume-v1=" + Array.from(mac, (b) => b.toString(16).padStart(2, "0")).join("");
let ok = false;
for (const part of header.split(",")) {
const got = part.trim();
let diff = got.length ^ expected.length;
for (let i = 0; i < expected.length; i++) diff |= (got.charCodeAt(i) || 0) ^ expected.charCodeAt(i);
if (diff === 0) ok = true; // check every entry, no early exit
}
return ok;
}Where this bites
The failure is silent from your side: you rotate, your endpoint starts answering 401 to genuine deliveries, and Sume retries each one up to 10 times at fixed 30-second gaps. A 401 is not a bug in your route; it is the verifier. The delivery receipt also carries x-sume-webhook-secret-fingerprint, and comparing it with the fingerprint beside the secret in the dashboard tells you whether you hold the new secret.
Two safe orders for a rotation: swap in a multi-entry verifier first, then rotate; or rotate and accept that the first 24 hours of deliveries need the redeliver endpoint (POST /v1/jobs/{job_id}/webhook/redeliver) once you have fixed the receiver.
Limits
This post covers a mismatch between the published tarball and the repository on the date shown. A newer npm release may fix it; check the installed version's dist/verify-webhook.js for a comma split before dropping the local file. The local verifier checks the sume-v1= scheme only, so a future scheme version would need a code change.
Sources
Related posts
More in Developers
- Does the Sume SDK retry POSTs? Only with an Idempotency-Key
createSumeClient retries 408, 429 and 5xx twice with backoff, but replays a POST only when it carries an Idempotency-Key. How to set it and tune maxRetries.
- @sume-com/sdk waitForJob is not exported: a 26-line replacement
The docs show waitForJob, but the npm build of @sume-com/sdk 0.2.0 does not export it. Here is a fetch version with 429 tolerance and a deadline.
- Check an Avatar payload with sume tools schema before --confirm-paid
sume tools schema avatar-videos.create --json prints the exact fields before you pass --confirm-paid. A read-only pre-flight loop for a spending CLI command.
- Cost of one Sume agent thread or turn: /v1/usage thread_id and job_id
Pass thread_id to GET /v1/usage to sum one Studio Agent thread, or a turn's job id as job_id for that turn plus every job it commissioned. Fields and a request.
Written by Sume