Verifying webhooks

Sume signs every webhook delivery with HMAC-SHA256 over <timestamp>.<raw_body> and sends it as sume-v1=<hex>. verifyWebhook is that check, so you do not write it.

Read your signing secret on the Webhooks tab of the dashboard (/dashboard/webhooks — Reveal, then copy), or from GET /v1/webhooks/signing-secret with any API key carrying account:read. It is derived for your workspace, so a valid signature proves the delivery was signed for you rather than for anyone holding a shared platform secret. Store it the way you store the API key. Use the env name SUME_COM_WEBHOOK_SIGNING_SECRET so local samples match what Sume's worker signs with. It is not the API key, and the client is not involved: verifyWebhook takes no client and makes no request.

The dashboard also shows a fingerprint of the secret, and every delivery carries the same value as x-sume-webhook-secret-fingerprint. When a signature will not verify, compare fingerprints — it is the one part of this that is safe to paste into a ticket.

Rotating the secret

If your secret may have leaked, rotate it: Webhooks → Rotate secret, or POST /v1/webhooks/signing-secret/rotate with a key carrying account:write.

Rotation is not a cutover. For 24 hours afterwards Sume signs every delivery with both secrets and sends them comma-separated in x-sume-webhook-signature, newest first:

A receiver holding either secret verifies, so you can redeploy on your own schedule rather than in the same instant you press the button. After the window the old secret stops verifying. The dashboard shows the deadline while the window is open, and rotation.previous_valid_until carries it on both API responses.

<Callout type="warn"> `verifyWebhook` handles the multi-signature header from **`@sume-com/sdk` 0.5.0**. An older verifier — or a hand-rolled one that compares the header for equality — fails on every delivery during the window. Upgrade the receiver *before* you rotate. Outside a window exactly one signature is sent, so nothing changes for anyone who never rotates. </Callout>

x-sume-webhook-secret-fingerprint names the new secret from the moment you rotate, including during the window. It tells you which secret to move to, not which ones are still accepted. Rotating twice inside one window retires the secret two rotations back immediately, which is what makes a leak actually stop.

Delivery is live on api.dev.sume.com and api.sume.com. Polling or subscribeFormatRun remains a valid backup. See Run webhooks.

Input

FieldNotes
bodyThe raw body: string, ArrayBuffer, or a typed array.
headersA Headers, a Map, or a plain object (Node's req.headers). Case-insensitive.
secretYour Sume webhook signing secret.
toleranceSecondsReplay window. Default 300. 0 skips the timestamp check.

The two headers it reads:

Four rules that decide whether this works

  • Pass the raw body. A parsed-and-reserialized object does not verify — key order and whitespace are part of what was signed. Frameworks that parse JSON for you have already destroyed the bytes. In Express, mount express.raw({ type: "application/json" }) on the webhook route only. In Next.js App Router, await request.text() before anything else.
  • It is async. The implementation uses WebCrypto rather than node:crypto, which is what keeps the package importable from Workers, Deno, and bundlers that refuse node: specifiers. await it.
  • It returns false rather than throwing on a malformed delivery. A missing header, a garbage timestamp, and a wrong signature are all just failed verification — one thing to branch on, no try/catch.
  • Comparison is constant-time, and the replay window is enforced before the HMAC is computed at all.

One verifier, two surfaces

Run webhooks (*.run.terminal, carrying run_id) and generation-job webhooks (job.*, carrying job_id) share the sume-v1 scheme exactly. The payloads differ; the signature does not. So one verifier covers both — route on event, and never assume a body has run_id:

Treating an unrecognized event as 204 is what stops a newly added event type from becoming a 500 and a retry storm.

Constants, if you need them

verifyWebhook covers the common case. The pieces are exported for when you do not control the receiver — a gateway that verifies before your code runs, say:

ExportValue
SUME_WEBHOOK_SIGNATURE_VERSION"sume-v1"
SUME_WEBHOOK_SIGNATURE_HEADER"x-sume-webhook-signature"
SUME_WEBHOOK_TIMESTAMP_HEADER"x-sume-webhook-timestamp"
DEFAULT_SUME_WEBHOOK_TOLERANCE_SECONDS300

Next

  • Run webhooks — delivery, events, payloads, retries, and the raw scheme for non-JavaScript receivers
  • Webhooks — generation-job webhooks, the other surface
  • Waiting for runs — the poll path (required on production until delivery is enabled; optional fallback on api.dev)