·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:
- Receivers can log + replay raw bodies. JWT's base64-wrapping means logs need to decode before they're useful.
- HMAC is faster on the receiver. No JWT library, no parsing — just a hash + constant-time compare.
- 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));