HMAC webhook signing for crypto payment processors, explained · Geat.io Blog
← Back to blog
Engineering

HMAC webhook signing for crypto payment processors, explained

June 10, 2026·7 min read

Every reputable crypto payment processor signs its webhook callbacks. The reason is mechanical: a payment processor that simply POSTs "invoice paid" to your server with no proof of origin is asking you to credit orders based on whoever happens to know your callback URL. That URL almost always leaks — through logs, through proxy headers, through the merchant pasting it in a Slack message.

HMAC-SHA256 webhook signing closes that hole. Done right, it means an attacker can know your URL, your invoice IDs, and even your network traffic, and still cannot forge a valid "this invoice is paid" message.

What the signature actually commits to

Every well-designed scheme has two non-negotiable inputs:

  • ·The raw HTTP body — the exact bytes the processor sent, not your framework's parsed JSON.
  • ·A Unix timestamp included as a separate header, also covered by the signature.

Concretely, the processor computes:

signature = "sha256=" + hex(hmac_sha256(secret, timestamp + "." + raw_body))

Your server, knowing the same secret, recomputes the signature from the received timestamp + body and compares the two with a constant-time comparison. If they match, the message is authentic. If they don't, the message is rejected with 401.

Why the timestamp matters

Without a timestamp, an attacker who once captured a valid webhook (say, from a leaked log) could replay it forever to make your store re-credit the same order. The timestamp window — typically ±300 seconds — kills that attack: the processor includes "when this message was sent" inside the signed payload, your server rejects anything older than the window, and the attacker's captured payload becomes worthless within five minutes.

The window must be small enough to make replay impractical but wide enough to absorb normal clock skew between the processor's servers and yours. 300 seconds is the de facto standard across Stripe, BitPay, and most modern processors.

The four ways to get this wrong

These are the implementation footguns that show up in production code:

  • ·Verifying against the parsed JSON instead of the raw body. The signature commits to bytes, not objects. If your web framework parses the JSON first and you sign against the result of re-serialising it, the bytes won't match even if the payload is genuine. Always read php://input or req.rawBody before any middleware touches it.
  • ·String equality (== or strcmp) for the signature comparison instead of constant-time. A timing attack can recover the signature byte-by-byte if comparison short-circuits. Use hash_equals (PHP), crypto.timingSafeEqual (Node), hmac.compare_digest (Python).
  • ·Skipping the timestamp window because "the signature is enough". Without timestamp + window, replay is trivial — the signature stays valid forever.
  • ·Storing the webhook secret in client-side code or env files that get committed. The secret must be server-only. If it leaks, rotate it immediately on the processor dashboard.

A note on idempotency

Webhook signatures protect authenticity, but processors will deliver the same signed webhook more than once on purpose — that's how retry logic works when your server is briefly unreachable. The signature being valid says "this is authentic." Your handler still has to be idempotent, meaning crediting the same invoice twice in a row is a no-op, not a double-payment.

The simplest pattern: include the invoice ID and an event type in your dedup key, store the dedup record on a successful credit, and short-circuit if the same key arrives again. Then return 2xx so the processor stops retrying.

The summary

A correctly-implemented HMAC webhook verification is about 20 lines of code in any language. The four ways to do it wrong are all well-known. If your payment processor doesn't sign webhooks, that's not a value-engineering choice — that's a security problem.

Want to try this on your own store?

Geat.io is a crypto payment gateway for online businesses. No percentage fees, no KYC, five-minute setup.

Start free →