Sume webhooks in a Cloudflare Worker: verify, queue, 204

A Worker that verifies a Sume delivery with verifyWebhook, hands the event to a queue with ctx.waitUntil, and replies 204 inside Sume's 10-second limit.

4 min readSume
All posts

Verify the delivery with verifyWebhook, answer 204 right away, and hand the slow work to a queue through ctx.waitUntil. Sume gives each delivery attempt 10 seconds, retries up to 10 times 30 seconds apart, and does not follow redirects. A Worker that does its processing before it replies risks a timeout and a duplicate. The SDK helper uses WebCrypto, so it runs in a Worker without node:crypto.

What Sume expects from the receiver

The SDK page states that verifyWebhook is async, returns false instead of throwing, compares in constant time, and enforces the replay window before it computes the HMAC.

Delivery behavior from the webhook docs, read 2026-10-08
ItemBehavior
AttemptsUp to 10, 30 seconds apart
Timeout10 seconds for each attempt
RedirectsNot followed
Expected replyA fast 2xx
Dedupe keyjob_id or request_id

The Worker

The code reads the secret from the Worker environment and returns 500 if it is missing, so a deploy without the secret fails loudly instead of accepting forged calls. SUME_EVENTS is a queue producer binding that you declare in your Worker configuration. Replace the send call with any store that supports a unique key.

import { verifyWebhook } from "@sume-com/sdk";

export default {
  async fetch(request, env, ctx) {
    const secret = env.SUME_COM_WEBHOOK_SIGNING_SECRET;
    if (!secret) return new Response("secret not configured", { status: 500 });
    if (request.method !== "POST") return new Response(null, { status: 405 });

    const body = await request.text(); // raw text 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);
    const id = event.job_id ?? event.run_id ?? event.request_id;
    // Answer inside the 10 second limit; do the slow work after the reply.
    ctx.waitUntil(env.SUME_EVENTS.send({ event: event.event, id, body }));
    return new Response(null, { status: 204 });
  },
};

Operational notes

Test the deployed URL with POST /v1/webhooks/test-deliveries. A 204 on the dummy webhook.test event shows the signature check, the parse and the reply all work.

  • Set the secret with your platform's secret store, under the name SUME_COM_WEBHOOK_SIGNING_SECRET, the name used in the Sume docs.
  • Do not call JSON.parse before you verify. Verify the raw text, then parse.
  • Return 204 for event names you do not know, so a new event type does not cause retries.
  • Keep polling as a backup for jobs whose terminal event never arrived.

What to store

Send the raw body along with the id, not a re-serialized object. If a later consumer needs to verify or replay the event, the original bytes are the only faithful copy. The consumer should insert with a unique key on the id, so a retried delivery that arrives while the first is still queued does not run the work twice.

For run webhooks, the receipt can be large. When it passes 1 MiB, the payload is null, the error code is payload_too_large, and a result_url tells you where to fetch the receipt.

Sources

Related posts

More in Developers

All Developers posts

Written by Sume