Mobidoo

Verifying webhooks

verifyWebhook in @sume-com/client checks the sume-v1 signature on a Sume delivery — raw bodies, header shapes, the replay window, and secret rotation.

Sume signs each webhook delivery with HMAC-SHA256 over <timestamp>.<raw_body> and sends the signature as sume-v1=<hex>. verifyWebhook in @sume-com/client does that check. Thus, you do not write it.

Read your signing secret on the Webhooks tab of the dashboard (/dashboard/webhooks — Reveal, then copy). You can also read it from GET /v1/webhooks/signing-secret with an API key that has account:read. Sume derives the secret for your workspace. Thus, a valid signature proves that Sume signed the delivery for you, not for any holder of a shared platform secret. Store the secret the same way that you store the API key.

Use the env name SUME_COM_WEBHOOK_SIGNING_SECRET, so that your local samples use the same name as the Sume worker that signs. The secret is not the API key. verifyWebhook needs no client and makes no request.

The dashboard also shows a fingerprint of the secret. Each delivery carries the same value as x-sume-webhook-secret-fingerprint. When a signature does not verify, compare the fingerprints. The fingerprint is the only part of this data that is safe to paste into a ticket.

Rotating the secret

If it is possible that your secret leaked, rotate it. Use Webhooks → Rotate secret, or POST /v1/webhooks/signing-secret/rotate with a key that has account:write.

A rotation is not a cutover. For 24 hours after the rotation, Sume signs each delivery with both secrets. Sume sends the two signatures in x-sume-webhook-signature, with a comma between them and the newest first:

A receiver with one of the two secrets can verify the delivery. Thus, you can redeploy on your own schedule, not at the same instant that you push the button. After the window, a check with the old secret fails. The dashboard shows the deadline while the window is open. rotation.previous_valid_until gives the deadline on both API responses.

<Callout type="warn"> CAUTION: Upgrade the receiver *before* you rotate. A hand-rolled verifier that compares the header for equality fails on each delivery during the window. `verifyWebhook` in **`@sume-com/client` 0.3.0** already handles the multi-signature header: it accepts the delivery when any one signature matches. If you never rotate, nothing changes, because Sume sends only one signature outside a window. </Callout>

x-sume-webhook-secret-fingerprint names the new secret from the moment that you rotate, and also during the window. It tells you the secret to move to. It does not tell you which secrets Sume still accepts. If you rotate two times in one window, Sume immediately retires the secret from two rotations before. This is how you make a leak really stop.

Delivery is live on api.dev.sume.com and api.sume.com. A poll loop stays a valid backup. Refer to Run webhooks.

Input

verifyWebhook(rawBody, timestamp, signature, secret, toleranceSeconds?)

ArgumentNotes
rawBodyThe raw body: a string or a Buffer.
timestampThe x-sume-webhook-timestamp header. Node's req.headers[...] value works as is.
signatureThe x-sume-webhook-signature header.
secretYour Sume webhook signing secret. An empty secret throws BadInputError.
toleranceSecondsReplay window. Default 300.

The two headers look like this:

Four rules that decide whether this works

  • Pass the raw body. A parsed-and-reserialized object does not verify, because the key order and the whitespace are part of the signed data. A framework that parses JSON for you destroyed the bytes before your code gets them. In Express, mount express.raw({ type: "application/json" }) on the webhook route only. In Next.js App Router, await request.text() before all other steps.
  • It runs on Node.js. The check uses node:crypto, and it returns at once: there is nothing to await. On Workers or Deno, verify with WebCrypto by the raw scheme in Run webhooks.
  • It returns false and does not throw on a malformed delivery. A missing header, a bad timestamp, a stale timestamp and a wrong signature all give only a failed verification. You branch on one value.
  • Comparison is constant-time. The replay window is checked before the HMAC.

One verifier, two surfaces

Run webhooks (*.run.terminal, with run_id) and generation-job webhooks (job.*, with job_id) use exactly the same sume-v1 scheme. The payloads are different, but the signature is the same. Thus, one verifier covers both. Route on event, and never expect that a body has run_id:

When you treat an unrecognized event as 204, a new event type does not cause a 500 and a retry storm.

Constants, if you need them

verifyWebhook covers the usual case. The package also exports the pieces, for when you do not control the receiver. An example is a gateway that verifies before your code runs:

ExportValue
WEBHOOK_SIGNATURE_VERSION"sume-v1"
WEBHOOK_SIGNATURE_HEADER"x-sume-webhook-signature"
WEBHOOK_TIMESTAMP_HEADER"x-sume-webhook-timestamp"
WEBHOOK_TOLERANCE_SECONDS300

Python has no verifier in sume-client. The Cookbook has a complete Python receiver.

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 and jobs — the poll path (an optional fallback, because delivery is live on production)