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.

5 min readSume
All posts

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.

How verifyWebhook reads the signature header (Sume SDK source and docs, read 2026-10-04)
Header valueResult
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> onlyfalse, no sume-v1 entry to compare
empty or missing headerfalse

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

All Developers posts

Written by Sume