Workers post-quantum Web Crypto: Sume webhooks stay HMAC-SHA-256

Cloudflare Workers Web Crypto now supports ML-KEM and ML-DSA. Sume webhook signatures are still HMAC SHA-256, and verifyWebhook runs on Workers with WebCrypto.

4 min readSume
All posts

Cloudflare's Sep 28, 2026 changelog says Workers Web Crypto now supports ML-KEM-768, ML-KEM-1024, ML-DSA-44, ML-DSA-65 and ML-DSA-87. That does not change how you verify a Sume webhook: the signature is still HMAC SHA-256 over the timestamp and raw body, and verifyWebhook from @sume-com/sdk runs on Workers using WebCrypto.

What changed on Workers, and what did not

The new algorithms are for key encapsulation (ML-KEM) and digital signatures (ML-DSA). They are additions to what a Worker can do; nothing in Sume's delivery format moved to them.

Cloudflare Workers Web Crypto addition and Sume webhook scheme (read 2026-10-03)
ItemValue
New in Workers Web Crypto (Sep 28, 2026)ML-KEM-768, ML-KEM-1024, ML-DSA-44, ML-DSA-65, ML-DSA-87
Sume webhook signatureHMAC SHA-256 over <timestamp>.<raw_body>
Signature headerx-sume-webhook-signature: sume-v1=<hex>
Timestamp headerx-sume-webhook-timestamp

Sume's scheme is a shared-secret MAC

HMAC uses a shared secret that you and Sume both hold, so verification is symmetric and has no public key. Sume derives the signing secret per workspace; read it from the Webhooks tab or GET /v1/webhooks/signing-secret, and store it as SUME_COM_WEBHOOK_SIGNING_SECRET.

Nothing about post-quantum signatures changes that choice for you. If Sume ever changes its scheme, the docs will say so; until then, keep verifying the sume-v1= header.

A Worker that verifies and acks

The SDK check reads the raw body before any JSON parsing, compares against every sume-v1= entry during a secret rotation and rejects a stale timestamp. Return a fast 2xx after storing the event, and use job_id as your idempotency key because deliveries can repeat. The sketch refuses to run with an empty secret.

  • Read the raw body first.
  • Refuse an empty secret.
  • Ack with 2xx fast; process afterwards.
  • Keep status_url polling as a fallback.
import { verifyWebhook } from "@sume-com/sdk";

export default {
  async fetch(request: Request, env: { SUME_COM_WEBHOOK_SIGNING_SECRET?: string }) {
    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 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);
    // store event keyed by event.job_id, then ack
    return new Response(null, { status: 204 });
  },
};

Sources

Related posts

More in Developers

All Developers posts

Written by Sume