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.

5 min readSume
All posts

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.

verifyWebhook inputs and behavior, as of 2026-10-09.
ItemValue
bodyThe raw body: string, ArrayBuffer, or typed array
headersA Headers, a Map, or a plain object; case-insensitive
secretYour Sume webhook signing secret
toleranceSecondsReplay window; default 300; 0 skips the timestamp check
Return valuetrue or false; it does not throw on a malformed delivery
RotationTwo 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

All Developers posts

Written by Sume