A Sume webhook receiver in a Cloudflare Worker with verifyWebhook
verifyWebhook uses WebCrypto, not node:crypto, so it can run in a Worker. A fetch handler that refuses an empty secret, checks the signature, returns 204.

Yes, you can verify Sume webhooks in a Worker. Sume's verifyWebhook is async and built on WebCrypto, not node:crypto, and the SDK docs say that is so you can import the package from Workers, Deno, and bundlers that refuse node: specifiers. A Worker fetch handler that reads the raw body, calls verifyWebhook, and returns 204 is all you need.
The handler below also refuses to run with an empty secret. Without that check, a missing binding becomes an undefined secret and a confusing failure on every delivery.
What the verifier needs
Inputs and rules from the Verifying webhooks page, read 2026-10-09.
| Item | Value |
|---|---|
body | The raw body: string, ArrayBuffer, or typed array |
headers | A Headers, a Map, or a plain object; case-insensitive |
secret | Your Sume webhook signing secret |
toleranceSeconds | Replay window; default 300; 0 skips the timestamp check |
| Return value | true or false; it does not throw on a malformed delivery |
| Rotation | Two sume-v1= entries for 24 hours, newest first; either verifies |
The Worker
Store the secret as a Worker secret named SUME_COM_WEBHOOK_SIGNING_SECRET, the same name the Sume delivery worker uses. Get the value from the Webhooks tab of the dashboard, or from GET /v1/webhooks/signing-secret with a key that has account:read. It is not your API key.
import { verifyWebhook } from "@sume-com/sdk";
interface Env {
SUME_COM_WEBHOOK_SIGNING_SECRET: string;
}
export default {
async fetch(request: Request, env: Env): Promise<Response> {
if (request.method !== "POST") return new Response(null, { status: 405 });
const secret = env.SUME_COM_WEBHOOK_SIGNING_SECRET;
if (!secret) return new Response("secret not configured", { status: 500 });
const body = await request.text(); // raw, before any JSON.parse
const ok = await verifyWebhook({ body, headers: request.headers, secret });
if (!ok) return new Response("bad signature", { status: 401 });
const event = JSON.parse(body);
console.log(event.event, event.request_id); // enqueue real work here
return new Response(null, { status: 204 });
},
};Things that still bite
Read the body as text first. A framework or a middleware that parses JSON and re-serializes it changes the key order and whitespace, and the signature covers the exact bytes. The helper checks the replay window before it computes the HMAC, and it compares in constant time.
A 401 from this handler counts as a failed attempt, so Sume retries, up to 10 attempts with a 30 second gap and a 10 second timeout per attempt. If signatures stop verifying after a rotation, compare the x-sume-webhook-secret-fingerprint header with the fingerprint next to the secret in the dashboard. It is the one value that is safe to paste into a support ticket.
The handler does not branch on the event name on purpose. Keep the Worker thin: verify, record, answer. The decision about what a job.completed or a job.canceled means for your product belongs in the code that reads the queue, where it can retry on its own schedule and not inside a 10 second delivery attempt.
Finally, upgrade the receiver before you rotate. During the 24 hour window the header carries two signatures, and a hand-written equality check fails on every delivery. verifyWebhook already accepts either.
Sources
Related posts
More in Developers
- Sume webhook retries: 10 attempts, 270 seconds of gaps, then poll
How long does Sume keep retrying a job webhook? Ten attempts, a 30 second default gap and a 10 second timeout, worked out in seconds, plus a Python dedupe.
- Route Sume job webhooks: handle job.canceled, answer 204 to the rest
Sume sends job.completed, job.failed and job.canceled; run webhooks use another name. A TypeScript router that handles each and returns 204 for the rest.
- Sume webhook signature fails: compare the secret fingerprint first
Webhook signature mismatch? Compare x-sume-webhook-secret-fingerprint with the dashboard before touching code. It is the one value safe to paste in a ticket.
- Sume webhook_url without a mode field: it runs in webhook mode
Send webhook_url and omit mode, and Sume treats the submit as mode webhook: 202, job id in the first response, signed terminal callback, poll as backup.
Written by Sume