DEV Community

Peter Panda
Peter Panda

Posted on

Verifying Crypto Payment Webhooks: An HMAC Checklist for Merchant Developers

When a merchant adds a crypto payment gateway, the most security-sensitive code is usually not the checkout button. It is the webhook handler that marks an order as paid. If that endpoint trusts whatever it receives, anyone who discovers the URL can mark orders as paid. This post is a short checklist we hand to developers on merchant teams.

1. Verify the signature on the raw body

Most gateways sign the webhook payload with a shared secret using HMAC-SHA256 and send the result in a header. Compute the signature over the raw request body, before any JSON parsing or re-serialization, and compare with a constant-time function.

import hmac, hashlib

def verify(raw_body: bytes, header_sig: str, secret: str) -> bool:
    expected = hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, header_sig)
Enter fullscreen mode Exit fullscreen mode

Frameworks that parse JSON automatically can break this. In Express, use express.raw() for the webhook route; in Django, read request.body.

2. Check the timestamp

If the gateway includes a timestamp, reject events older than a few minutes. That limits replay attacks where someone resends a captured valid payload.

3. Make the handler idempotent

Gateways retry. Store the event ID and ignore duplicates. Your "mark as paid" logic should be safe to run twice without shipping twice.

4. Do not trust the amount in the webhook alone

For high-value orders, call the gateway API to fetch the payment by ID and confirm amount, currency and status. The webhook tells you something happened; the API tells you what.

5. Handle partial and over-payments explicitly

Crypto customers sometimes send slightly less (fees deducted by their wallet) or more. Decide a tolerance rule, write it down, and implement it. Orders outside the tolerance go to a manual review queue rather than auto-fulfilment.

6. Respond fast, process later

Return a 2xx quickly and push work to a queue. Slow handlers cause timeouts, which cause retries, which cause duplicates.

7. Log enough to reconcile

Log event ID, order ID, transaction hash, amount, network and the verification result. Finance will need these when matching gateway reports to orders at month end.

8. Rotate the secret

Treat the webhook secret like any credential: keep it in environment variables or a secrets manager, never in the repo, and rotate it when team members leave.

A minimal handler shape

def handle(request):
    raw = request.body
    if not verify(raw, request.headers.get("X-Signature", ""), SECRET):
        return 401
    event = json.loads(raw)
    if seen(event["id"]):
        return 200
    enqueue(event)
    mark_seen(event["id"])
    return 200
Enter fullscreen mode Exit fullscreen mode

For a broader overview of how gateways work, settlement options and what to ask a provider, see a crypto payment gateway guide. Our team at PandaWM, based in Hong Kong, helps overseas merchants set up gateway integrations; the gateway we work with serves non-mainland merchants only.

Published by PandaWM, an overseas marketing team based in Hong Kong.

Top comments (0)