We manage your security to weather the AI storm →

← Resource Hub

Webhook Security Best Practices for Crypto Payments

By Owen Thurm Published 6 min read

Webhook Security Best Practices for Crypto Payments

Quick answer. Treat every webhook as a hint, not proof. Verify its signature over the raw body in constant time, reject stale timestamps, process each event once, and confirm the token contract, amount, recipient and confirmations onchain before crediting.

Watch: Webhook Security Best Practices for Crypto Payments 1:38
Video transcript

If someone forges or replays a payment webhook, your backend credits a deposit that never happened. Treat every webhook as a hint, not proof.

A webhook endpoint is a public URL that changes balances. Attackers use forgery and replay, and duplicates arrive anyway, because providers retry.

Whatever the provider's scheme, verify the signature over the raw body, compare in constant time, pin the scheme, guard the secret, and fail closed.

A valid signature proves who sent a message, not when. Stripe's libraries reject events more than five minutes old by default.

Next, credit each transfer once. Record event IDs, key the credit on chain, transaction hash and log index, then save the credit before you acknowledge.

The webhook is a notification. The chain is the record. Before crediting, check status, token contract, recipient, delivered amount, network and confirmations.

Match the contract address, never the symbol. Read what was actually delivered, not the stated amount. And watch for reorgs that remove a transfer.

Confirmations depend on the chain. In Guardian's Nunchi review, fifteen block confirmations were hardcoded for every chain. Guardian recommended a setting per chain.

Guardian's API and web application pentests test replay, reordering and confirmations in value-moving flows. Read the full testing checklist, or talk to Guardian at guardian.security.

A payment webhook tells your backend that money arrived. If an attacker can forge or replay one, your system credits a deposit that never happened.

Stripe puts the risk plainly: without verification, "an attacker could send fake webhook events to your endpoint" and trigger actions like fulfilling orders.

Crypto payments offer a second check: the chain itself can tell you whether the payment is real. Use it. For the wider setup, see how to accept stablecoin payments securely.

Why Payment Webhooks Are Targets

A webhook endpoint is a public URL that changes balances. It can be abused in three ways.

  • Forgery: an unsigned or badly checked request looks like a real deposit.
  • Replay: a captured, validly signed event is sent again to credit twice.
  • Duplicates: providers retry, so even honest traffic repeats.

Duplicates are normal. Stripe says endpoints "might occasionally receive the same event more than once." Coinbase's developer platform promises "at-least-once delivery for every event."

Order is not guaranteed either. Stripe "doesn't guarantee the delivery of events in the order that they're generated."

Internal event streams have the same problem. In Guardian's Synthetix withdrawal flow review (M-02), a redelivered deposit event could be applied twice, "inflating the user's collateral balance."

A webhook can also be honest and still wrong. A chain watcher may report a fake token, or a transfer that a reorg later removes.

Signature Verification

Providers sign webhooks so you can prove who sent them. The schemes differ.

ProviderHeaderSchemeFreshness
StripeStripe-SignatureHMAC-SHA256 over timestamp and body5-minute default tolerance
Coinbase Developer PlatformX-Webhook-SignatureHMAC-SHA256 over timestamp and body (v1 adds headers)5-minute window in its example
CircleX-Circle-Signature, X-Circle-Key-IdECDSA with SHA-256, public key fetched by IDNot covered on its verification page
AlchemyX-Alchemy-SignatureHMAC-SHA256 over the bodyNot covered; source IPs published

Whatever the scheme, the same rules apply:

  • Use the raw body. Stripe, Circle and Alchemy all warn that parsed or re-serialized JSON breaks verification.
  • Compare in constant time. Stripe and Coinbase both call for a timing-safe comparison.
  • Pin the scheme. Stripe says to ignore schemes other than v1, to prevent downgrade attacks.
  • Guard the secret. One per endpoint, out of code and logs, rolled on a schedule.
  • Fail closed. A missing header or bad signature gets a 4xx and no credit.

Stripe lets old and new secrets overlap for up to 24 hours when you roll them. Circle uses asymmetric keys, so there is no shared secret to leak; its docs say to cache the public key.

Replay Protection

A valid signature proves who sent a message, not when. Two controls close the gap.

Timestamp tolerance

Stripe signs a timestamp with the body, so an attacker cannot change it. Its libraries reject events more than 5 minutes old by default.

Coinbase's example uses the same window, and a shorter one, such as 1 minute, for high-security applications. Stripe warns that a tolerance of 0 disables the check entirely.

Keep server clocks synced with NTP, as Stripe advises.

Idempotency

Inside the window, the same event can still arrive twice. Record each event ID and skip any you have already processed, as Stripe recommends.

For crypto, also key the credit on the transfer itself: chain, transaction hash and log index. Two events about one transfer can then never credit twice.

Enforce the window, and keep that record longer than it. In the same Synthetix review (H-13), a signed authentication message had no freshness check; its timestamp was held as a nonce for only 10 minutes.

Once that record expired, the same signature could be replayed.

Acknowledge only after the credit is saved. In M-16, deposits were acknowledged before processing, so a crash in between meant funds could arrive onchain and never be credited.

Verifying Against Chain State

For crypto payments, the webhook is a notification. The chain is the record.

Before crediting, read the transaction from your own node or a second provider, and check:

  • Status: it succeeded and is on the canonical chain.
  • Token contract: the exact contract address you accept, never the symbol.
  • Recipient: an address you issued to this customer.
  • Amount: what was actually delivered, in the token's decimals.
  • Network: the chain you expect, not a testnet or another chain.
  • Confirmations: enough for your finality policy on that chain.

Symbols are not identity. Alchemy's address activity payload reports the asset as "ETH or the token's symbol," with the contract address in a separate field. Match on the address.

The stated amount can differ from what arrived. XRP Ledger docs describe a partial payment exploit: an integration that reads the Amount field credits far more than it received. The fix is the delivered_amount field.

Reorgs can undo a transfer. Alchemy's payload carries a removed flag for a transaction "no longer in the canonical chain."

How long to wait depends on the chain; see settlement finality by chain. In Guardian's Nunchi review (M-02), one count of 15 block confirmations was hardcoded for cross-chain reads on every chain. Guardian recommended a setting per chain.

Testing Checklist

Run these against staging before launch, and after any change to the handler.

  1. Send an unsigned request. Expect a 4xx and no credit.
  2. Sign with the wrong secret, then edit the body of a validly signed event. Expect both rejected.
  3. Replay a valid event after the tolerance window. Expect a rejection.
  4. Replay a valid event inside the window. Expect one credit, not two.
  5. Send two different events for one transaction hash. Expect one credit.
  6. Deliver events out of order, including a withdrawal before its deposit.
  7. Report a look-alike token with the right symbol. Expect no credit.
  8. Report a transfer to an address you never issued, or on the wrong chain.
  9. Report a transfer that later reorgs out. Expect the credit held, then reversed.
  10. Kill the worker between acknowledgment and credit. Expect no lost deposit.
  11. Search code, logs and the frontend bundle for the signing secret.
  12. If you allowlist provider IPs (Stripe and Alchemy publish theirs), send from another IP. Expect a rejection.

Stripe recommends IP allowlisting and signature verification together. Neither replaces the check against the chain.

How Guardian Tests It

Guardian's API pentest tests rate limits, quotas and replay protection, and whether they hold per key.

It also covers endpoints that fire relayers, bots or onchain transactions, and flows that can be replayed or reordered. Every endpoint is tested at each privilege level.

The web application pentest attacks value-moving flows: withdrawals, transfers, step order, replay, races and confirmations.

For more test cases by flow, see web application pentesting for crypto and fintech.

Frequently Asked Questions

How do I verify a crypto payment webhook?

Recompute the provider's signature over the raw request body, using your secret or its public key, and compare in constant time. Reject stale timestamps and repeated event IDs. Then confirm the transaction onchain before crediting.

Is HMAC enough to secure a webhook?

No. HMAC proves who sent the message, not that it is fresh, unique or true. Add a timestamp tolerance, record event IDs and check the payment onchain. A signed report of a fake-token transfer is still a fake deposit.

Should I allowlist webhook IP addresses?

Where the provider publishes them, as Stripe and Alchemy do, yes, as a second layer. Stripe recommends using IP allowlisting and signature verification together. An IP address alone does not prove an event is genuine.

How many confirmations should I wait before crediting?

It depends on each chain's finality. Set a policy per chain rather than one number for all, and hold the credit until it is met. Our settlement finality guide covers the major chains.

Sources

Make sure only real deposits get credited

Guardian forges, replays and reorders your payment events to find the path from a fake webhook to a real balance.