← Back to blog
·4 min·The SwyDex team

Webhook signing: why we use HMAC over JWT

Two ways to sign webhooks. Both are fine. We picked HMAC.

JWT: the webhook body is wrapped in a JWT signed with a per-tenant secret. Receiver verifies the JWT and trusts the body inside.

HMAC: the webhook body is sent as-is, with a separate header containing an HMAC-SHA256 of the body keyed by the per-tenant secret. Receiver re-computes the HMAC and compares.

HMAC wins for our use case for three reasons:

  1. Receivers can log + replay raw bodies. JWT's base64-wrapping means logs need to decode before they're useful.
  2. HMAC is faster on the receiver. No JWT library, no parsing — just a hash + constant-time compare.
  3. Less to get wrong. JWT's alg-confusion attacks are well-known. Plain HMAC has fewer footguns.

The downside: HMAC headers feel more “ad hoc” to some platforms. We compensate with clear docs and an SDK helper that does the verification in one call. Stripe-style, basically.

If you're consuming SwyDex webhooks, the verification is:

const expected = crypto.createHmac('sha256', WEBHOOK_SECRET)
  .update(rawBody).digest('hex');
crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));

More posts