Webhook Security: Why Every Callback Must Be Signed
The most common way a crypto payment integration loses money has nothing to do with cryptography. It is a server that believed a message it never checked.
Here is how a crypto payment integration actually loses money. Not a broken cipher, not a stolen key. A webhook endpoint that took a message at face value.
Your server needs to know when a payment lands, so the platform calls a URL on your side. If that call is not verified, anyone who finds the URL can post to it and tell your system an order was paid. Nothing was hacked. Your fulfilment logic was asked politely and said yes.
How signing fixes it
The platform and your server share a secret. Every callback carries a signature computed from the message body and that secret. Your server recomputes it and compares.
An attacker without the secret cannot produce a matching signature, so the forged message fails at the door, before any code that touches an order runs.
Getting it right
Verify the raw body
Compute the signature over the exact bytes received. Parsing to JSON and re-serialising changes whitespace and key order, and the signature will no longer match. Capture the raw body first, verify, then parse.
Compare in constant time
Use your language's constant-time comparison, not ==. Ordinary string comparison exits early on the first mismatched character, which leaks how much of a guess was correct. It is a subtle attack and the fix is one function call.
Verify before anything else
Before logging the contents, before touching the database, before any side effect. The first thing the handler does is decide whether this message is real.
Do not trust the payload for facts
Even a valid callback should be treated as a nudge rather than as truth. Verify the signature, then look the payment up by its identifier and act on what the platform says its state and amount are. That habit survives bugs on both sides.
The rest of the checklist
- Idempotency. Networks retry. The same callback will arrive twice, and the second one must be a no-op.
- Timestamps. Reject anything far outside a tolerance window so an old captured message cannot be replayed later.
- HTTPS only. Non-negotiable.
- Return 200 fast. Acknowledge, then do the slow work in a queue. Timing out invites a retry storm.
- Rotate the secret if it was ever pasted into a chat, a ticket or a log.
The one line to remember
Verify before you fulfil, never after. Every incident of this kind traces back to code that acted first and checked later, or never checked at all.
Frequently Asked Questions
What is a signed webhook?
A callback that carries a signature computed from the message body and a secret only you and the payment platform hold. Your server recomputes the signature and compares before acting, which proves the message came from the platform and was not altered.
Why do webhooks need to be signed?
Because the endpoint is on the public internet. Without verification, anyone who discovers or guesses the URL can post to it and tell your system an order was paid. No blockchain is attacked; your fulfilment logic is simply asked and it agrees.
How do I verify an HMAC signature?
Compute HMAC over the exact raw request body using your shared secret, then compare it to the signature header with a constant-time comparison. Verify before parsing or acting, and never use a plain equality check on the strings.
Should I trust the webhook payload for the amount?
Treat the callback as a signal rather than as truth. Verify the signature, then look the payment up by its identifier and act on what your platform says it is, not on the numbers inside the message.
