# How to verify crypto payment webhook signatures

> A webhook endpoint is a public URL that can mark orders as paid. Verifying the signature is what stops anyone but the gateway from doing that.

Updated 2026-09-30 · Payaries · https://www.payaries.com/guides/verify-crypto-webhook-signatures

## What arrives

**Headers**

```
X-Gateway-Signature: <hex HMAC-SHA256>
X-Gateway-Timestamp: <unix ms>

Signature = HMAC-SHA256(webhookSecret, timestamp + rawBody)
```

The signature covers the timestamp and the raw request body. Verify against the raw string, before any JSON parsing — re-serialising a parsed body can change whitespace or key order and break the match.

> **The webhook secret is not your API secret.** One verifies Payaries's calls to you; the other signs your calls to Payaries. Get or rotate the webhook secret from the dashboard — rotating invalidates the old one immediately, so plan a cutover.

## Verify it

**Node.js**

```
function verifyWebhook(rawBody, signature, timestamp, webhookSecret, toleranceMs = 5 * 60 * 1000) {
  if (Math.abs(Date.now() - timestamp) > toleranceMs) return false;
  const expected = crypto
    .createHmac('sha256', webhookSecret)
    .update(timestamp + rawBody)
    .digest('hex');
  const a = Buffer.from(expected, 'hex');
  const b = Buffer.from(signature, 'hex');
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}
```

- The timestamp check rejects old deliveries, which limits replay of a captured request.
- `timingSafeEqual` compares in constant time; check the buffer lengths first, because it throws on a length mismatch.

## A complete handler

**Express**

```
app.post('/webhooks/payaries', express.raw({ type: 'application/json' }), (req, res) => {
  const signature = req.header('X-Gateway-Signature');
  const timestamp = Number(req.header('X-Gateway-Timestamp'));
  const rawBody = req.body.toString('utf8');   // the RAW body — before any JSON parsing

  if (!verifyWebhook(rawBody, signature, timestamp, process.env.WEBHOOK_SECRET)) return res.sendStatus(401);

  const event = JSON.parse(rawBody);
  res.sendStatus(200);                          // acknowledge first, do the slow work after
  queue.add(event);                             // dedupe on event.idempotencyKey
});
```

Acknowledge with a `2xx` quickly — the endpoint has 10 seconds to respond — and do the slow work afterwards. A slow handler risks a needless retry even though you already received the event.

## Retries and duplicates

| Attempt | When |
|---|---|
| 1 | Immediately |
| 2 | After 1 minute |
| 3 | After 5 minutes |
| 4 | After 15 minutes |
| 5 | After 1 hour |
| Final | After 6 hours |

Because a delivery can repeat, deduplicate on the event's `idempotencyKey` and make your handler idempotent on the invoice and status. After the last failed attempt an invoice's `webhookStatus` becomes `failed`; re-fire it with `POST /v1/invoices/:id/resend-webhook`.

## Endpoint requirements

- A public `https://` URL. `http://`, localhost, private or link-local addresses and internal hostnames are refused, and re-checked on every delivery.
- Redirects aren't followed — register the final URL.
- Don't rely on the source address for authenticity. Always verify the signature.

## Questions

**Why must I verify the webhook against the raw body?**
The signature is computed over the exact bytes sent. Parsing and re-serialising the JSON can change whitespace or key order, so the recomputed HMAC no longer matches.

**How do I handle duplicate webhook deliveries?**
Deliveries are retried on failure, so the same event can arrive more than once. Deduplicate on the idempotencyKey and make your handler idempotent on the invoice and status.

**How fast must my webhook endpoint respond?**
Within 10 seconds with a 2xx status. Acknowledge first and process afterwards.

## Try it

Send yourself a signed test event: [Payaries — www.payaries.com](https://www.payaries.com/signup). Payaries is a multi-chain crypto payment gateway — https://www.payaries.com

## Keep reading

- https://www.payaries.com/crypto-payment-api — A crypto payment API that behaves like the rest of your stack
- https://www.payaries.com/guides/crypto-payment-confirmations — Crypto payment confirmations, explained for merchants
- https://www.payaries.com/guides/handle-underpaid-and-late-crypto-payments — Handling underpaid, late and unmatched crypto payments
- https://www.payaries.com/solutions/forex — A crypto payment gateway for forex brokers and trading firms
- https://www.payaries.com/ai-agents — Build a crypto payment integration with an AI agent, safely
- https://www.payaries.com/guides/sign-crypto-payment-api-requests — How to sign crypto payment API requests (HMAC-SHA256)
- https://www.payaries.com/guides/hosted-crypto-checkout — Hosted crypto checkout: send the customer, get them back
