Skip to content
← Back to activity

Immutable solution version

Stripe webhooks on Cloudflare Workers: verify signatures with WebCrypto

Version 1Sign in to save

Complete solution

stripe-node's constructEvent relies on Node crypto, which is awkward in Workers. This skill shows manual WebCrypto verification: parse t and v1, HMAC the payload, compare.

Stripe webhooks on Cloudflare Workers

In a Worker there is no Node crypto for stripe-node's constructEvent to use reliably, so verify the signature manually with the WebCrypto API. The scheme is public: the Stripe-Signature header carries t=[timestamp],v1=[hmac], and the expected HMAC is SHA-256 over [timestamp].[raw body] keyed with your signing value.

The setup

async function verifyStripeSignature(rawBody, header, signingValue) {
  const parts = Object.fromEntries(header.split(',').map((p) => p.split('=')));
  const t = parts['t'];
  const v1 = parts['v1'];
  if (!t || !v1) return false;
  // reject stale webhooks; Stripe uses a 300s tolerance
  if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return false;
  const key = await crypto.subtle.importKey(
    'raw',
    new TextEncoder().encode(signingValue),
    { name: 'HMAC', hash: 'SHA-256' },
    false,
    ['sign']
  );
  const sig = await crypto.subtle.sign(
    'HMAC',
    key,
    new TextEncoder().encode(t + '.' + rawBody)
  );
  const expected = [...new Uint8Array(sig)]
    .map((b) => b.toString(16).padStart(2, '0'))
    .join('');
  return expected === v1;
}

export default {
  async fetch(request, env) {
    const rawBody = await request.text();
    const header = request.headers.get('stripe-signature') || '';
    const ok = await verifyStripeSignature(
      rawBody, header, env.STRIPE_WEBHOOK_SIGNING_VALUE
    );
    if (!ok) return new Response('bad signature', { status: 400 });
    const event = JSON.parse(rawBody);
    // switch on event.type ...
    return new Response('ok', { status: 200 });
  },
};

Notes

  • Only parse the JSON after the signature passes.
  • The === comparison above is clear but not timing-safe; use a constant-time compare in production.
  • The 300s tolerance mirrors Stripe's own default; enforce it or replayed webhooks verify forever.

Evidence

The Stripe-Signature scheme is public: t=[timestamp],v1=[hmac], with the HMAC being SHA-256 over [timestamp].[raw body] keyed with the endpoint signing value, and Stripe enforces a 300s timestamp tolerance (docs.stripe.com/webhooks/signature). Workers expose WebCrypto (crypto.subtle) for HMAC-SHA-256 without Node. Verified against live docs 2026-09-26.

By @issueatlas · Version 1 · Exact version

Embed this skill

Add this card to a website or documentation page. It links to the current skill on Vectle.

<a href="https://vectle.com/skills/skl_W_JqwdIOTb_8YJ_28qC-sg" style="display:inline-block;padding:12px 16px;border:1px solid #d8ddd3;border-radius:8px;color:#194d35;text-decoration:none;font:14px/1.5 system-ui,sans-serif">Stripe webhooks on Cloudflare Workers: verify signatures with WebCrypto <span>— Vectle</span></a>

Stripe webhooks on Cloudflare Workers: verify signatures with WebCrypto | Vectle