verifyWebhook returns false when a header arrives as an array
verifyWebhook refuses an array-valued signature header unless it holds exactly one value. How to normalise headers so a rotation window still verifies.

verifyWebhook returns false, without throwing, when it is handed an object whose signature or timestamp header is an array with more than one entry. It treats a duplicated header as ambiguous and refuses to pick one. A single-entry array is fine. The fix is to pass it something it can read as one value per header: the framework's Headers, a Map, or a plain object of strings.
Which header shapes does verifyWebhook accept?
The headers input is a Headers, a Map, or a plain object such as Node's req.headers, matched case-insensitively. The two it reads are x-sume-webhook-timestamp and x-sume-webhook-signature.
| Value you pass | Result |
|---|---|
| Headers object | Reads the header by name |
| Map | Reads the exact name or its lower-case form |
| Plain object, string value | Used as is |
| Plain object, array with one string | That string |
| Plain object, array with two or more | Refused: false |
| Header missing, or empty secret | false |
Is a comma list the same as an array?
No, and this is where a secret rotation can bite. For 24 hours after you rotate the signing secret, Sume signs every delivery with both secrets and sends them in one header, comma-separated and newest first: sume-v1=<new>,sume-v1=<old>. That is a single header value, and verifyWebhook accepts the delivery when either entry matches. Outside a rotation window exactly one signature is sent.
What fails is a layer between Sume and your code that splits one comma-separated value into several array elements, or a gateway that repeats the header. The array then has two entries and the verifier returns false, which looks like a bad secret and is not.
Normalise before you verify
If you cannot change the layer that builds the array, join the entries back into the single comma-separated form the signature scheme uses. Verification still depends on the HMAC, so joining does not weaken it.
import { verifyWebhook } from "@sume-com/sdk";
const NAMES = ["x-sume-webhook-signature", "x-sume-webhook-timestamp"];
export function oneValuePerHeader(raw: Record<string, string | string[] | undefined>) {
const out: Record<string, string> = {};
for (const name of NAMES) {
const v = raw[name];
if (v === undefined) continue;
out[name] = Array.isArray(v) ? v.join(",") : v;
}
return out;
}
export async function accept(rawBody: string, rawHeaders, secret: string) {
if (!secret) throw new Error("SUME_COM_WEBHOOK_SIGNING_SECRET is empty");
return verifyWebhook({ body: rawBody, headers: oneValuePerHeader(rawHeaders), secret });
}What else returns false?
A malformed delivery never throws. A missing header, a non-numeric timestamp, a timestamp outside the replay window (300 seconds by default) and a wrong signature are all just failed verification. A signature entry with a different version prefix, such as a future sume-v2=, is dropped rather than compared, so it cannot make an older receiver fail. If the array is not your problem, compare x-sume-webhook-secret-fingerprint with the fingerprint on the dashboard, and make sure you pass the raw body.
How do I confirm it is the header shape and not the secret?
Log the typeof and length of both header values before you call the verifier. A string means the shape is fine. An array of two means a layer upstream is splitting or repeating the header. Then compare x-sume-webhook-secret-fingerprint with the fingerprint shown beside the secret on the Webhooks tab of the dashboard. Both sides can compare it without either ever sending the secret itself, and it is the one value safe to paste into a ticket.
A fingerprint match with a failing verification points at the body or the header handling. A mismatch points at the secret: the receiver may still hold the old one after a rotation, which is why the docs advise upgrading the receiver before you rotate.
Sources
Related posts
More in Developers
- Replay a saved Sume webhook in tests: verifyWebhook says false
A recorded delivery fails verifyWebhook once it is older than 300 seconds. Pin the clock with now or use toleranceSeconds 0 in tests, never in production.
- AI video API fallback: retry on another model when a job fails
Chain seedance-2.5, seedance-2 and kling-3 on Sume: poll status_url, read the job error category, and resubmit the brief to the next model.
- Sume video callback not verifying? Compare the secret fingerprint
Every Sume job webhook carries x-sume-webhook-secret-fingerprint. Compare it with the dashboard, accept two signatures in rotation, refuse an empty secret.
- Caption inputs on Sume: script_text, words, cues or segments?
Four caption inputs and only one may be sent. When to use script_text with transcription, word timings, or phrase cues for a silent clip. Plus the errors.
Written by Sume