Developers & APIs

Signed Webhooks for Crypto Payments: Verify Before You Ship Anything

Short answer: Signed webhooks for crypto payments are HMAC-authenticated callbacks your server verifies before trusting them. Payzum signs every payment event with your secret, so only genuine notifications can mark an order paid — and because settlement is non-custodial, the funds are already in your own wallet when the webhook arrives.

Key takeaways

  • Your webhook endpoint is a public URL that moves money. Anyone on the internet can POST to it, so the signature check is not a nicety — it is the authentication.
  • Verify first, parse second, act third. Recompute the HMAC over the raw body with a constant-time comparison, and reject anything that doesn't match before your order code ever sees the payload.
  • Idempotency matters more in crypto than on cards, because there is no chargeback to undo a double-fulfilled order. Key every side effect to the charge or event ID.
  • Payzum ships signed webhooks, encrypted secrets, 2FA and a full audit log, plus an integration playground where you can rehearse confirmations, expirations and overpayments before a real coin moves.

Why a payment webhook endpoint is the riskiest route in your app

Most of your backend is protected by a session, a token, or a private network. Your payment callback is none of those. It is a URL you publish, that accepts unsolicited POST requests from the open internet, and whose job is to tell your system that money arrived.

That combination is unusual, and teams underestimate it constantly. The three failure modes we see over and over:

  • Unauthenticated trust. The handler parses the JSON, reads status: "confirmed", and marks the order paid. Anyone who can guess the URL and the payload shape gets free goods. An IP allowlist helps a little; it is not a substitute for proof that this provider produced this exact payload.
  • Fulfilling from the browser. The customer gets redirected to /thanks?status=ok and the frontend calls an internal endpoint that flips the order. That is a URL the customer controls. Redirects are for user experience; webhooks are for truth.
  • Assuming exactly-once delivery. Webhook transports retry. A slow response, a deploy mid-request, a 502 from your load balancer — and the same event lands twice. If your handler isn't idempotent, you ship the order twice, or credit the wallet balance twice.

Crypto adds two more that card developers have never had to think about. First, the event you care about is an on-chain confirmation, not an authorization: there is no later "settled" event that quietly corrects an optimistic one. Second, the amount is not guaranteed to be the amount you asked for. Customers send from their own wallets, and they can send slightly less, slightly more, or on time but after your invoice expired.

What a sloppy webhook handler costs when payments are final

On card rails, a bad handler is expensive but survivable. Ship an order against a forged callback and you still have levers: the charge was never captured, or you refund it, or the acquirer reverses it. The reversibility that makes card payments painful for merchants also quietly forgives a lot of integration sin.

On-chain payments remove the safety net in both directions. Nobody can claw back your legitimate revenue — and nobody can claw back goods you shipped against an event you didn't verify. Concretely:

  • A forged "paid" event is a permanent loss. If an attacker can POST a fake confirmation, they get the digital download, the license key, the account upgrade, or the physical shipment, and there is no payment to reverse because there was never a payment.
  • A duplicate event is a permanent loss too. Two deliveries of the same confirmation, two credits to a customer balance, one payment received. At volume, "retry storms" during an outage turn a five-minute incident into a reconciliation project.
  • A missed event is a support queue. The customer paid, the chain confirmed, your endpoint was down for six minutes, and now a paying customer is staring at "awaiting payment". Every one of those becomes a ticket, and the ones who don't write in just churn.
  • Silent amount mismatches become spreadsheet work. Without explicit states for underpaid, overpaid and expired, every 49.97-instead-of-50.00 lands in a human's lap. We covered the wider version of this problem in our guide to building checkout on a REST API.

None of this is exotic engineering. It is roughly forty lines of handler code plus one architectural decision — trust nothing you haven't verified. But it has to be built before the first real payment, not after the first incident.

Why card-PSP webhook habits don't transfer to crypto payments

The card lifecycle trained a generation of backends to treat webhooks as status gossip. An authorization is provisional. A capture might come minutes or days later. Settlement is a batch. A dispute can reopen the whole thing up to roughly 120 days after the sale. In that world, the webhook is one signal among many, and the ledger of record is the processor's dashboard.

In a non-custodial crypto flow, the model inverts. There is one moment that matters: the payment confirms on-chain and the funds are in the merchant's wallet. There is no capture step, no settlement batch, no payout run, and no dispute window. The webhook is not gossip about a state that will be revised later — it is the notification that a final, irreversible transfer already happened.

That makes the webhook simultaneously simpler and more load-bearing. Simpler, because you are modelling fewer states. More load-bearing, because your handler is the only thing standing between a real on-chain event and your fulfillment logic — and it is the only thing standing between a forged HTTP request and the same fulfillment logic.

Custodial crypto gateways muddy this further. Their webhook says "paid", but the money is in their balance; you're still waiting on a payout schedule, a threshold, or a risk review. The event and the money have been decoupled again. That's the structural difference a non-custodial payment processor removes: when the webhook fires, the settlement has already happened, to an address you hold the keys to.

How Payzum's signed webhooks solve this

Payzum is a non-custodial, crypto-only payment processor. The developer surface is deliberately unsurprising — a REST API with API keys, and signed webhooks — and the parts that usually go wrong in crypto integrations are handled as platform behaviour rather than as your homework.

Every event carries a cryptographic signature

When a payment confirms on-chain, Payzum calls your endpoint with a signed webhook: a signature computed over the payload using your webhook secret. Your handler recomputes it and compares before trusting anything. This is the standard keyed-hash construction described in RFC 2104 — the same primitive behind the signing schemes used across payments infrastructure, and conceptually the same problem the IETF standardised more broadly in RFC 9421, HTTP Message Signatures.

The practical consequence: a request that doesn't carry a valid signature over the exact bytes you received cannot have come from Payzum, and your order code never has to look at it.

Secrets are treated like secrets

Webhook secrets and API keys live encrypted in the dashboard, the account supports 2FA, and every action lands in a full audit log. When you rotate a secret or someone changes an endpoint, there's a record of who did it and when — which is the difference between an incident you can explain and one you can only guess at.

Unambiguous events, because the charge object is unambiguous

Charges and invoices are first-class objects: an exact amount, your own reference, an expiration window, and overpayment detection. The awkward crypto edge cases stop being open-ended arithmetic in your handler and become defined states your code can branch on. Your webhook logic reacts to "this charge, this state"; it doesn't have to reverse-engineer intent from a transfer amount.

An integration playground before real money moves

You can rehearse the whole loop in the integration playground: create charges, trigger confirmations, receive and verify real signed webhooks, and exercise the expiration and overpayment paths. That means your payment flow can be covered by tests like any other part of your system — including the failure paths, which are the ones that matter here.

And the event means the money is already yours

The architectural point worth repeating: Payzum never holds the funds. Settlement goes on-chain, directly to a wallet you control, in seconds — roughly 0.4s on Solana, ~2s on Base and Polygon. Turn on auto-convert and whatever the customer paid with settles as USDC or USDT. When your handler marks the order paid, there is no payout state left to wait for.

How to handle a signed payment webhook, step by step

This is the checklist we walk through with engineering teams. It's short on purpose — the discipline is in the ordering, not the volume of code.

  1. Capture the raw body before anything parses it. Signatures are computed over exact bytes. If your framework re-serializes JSON before you get to it, key ordering or whitespace changes and every signature fails. In most stacks this means configuring a raw-body parser on the webhook route only.
  2. Verify the signature with a constant-time comparison. Recompute the HMAC over the raw body with your webhook secret, then compare using a timing-safe function (crypto.timingSafeEqual in Node, hmac.compare_digest in Python). A plain === leaks information a patient attacker can use. If verification fails, return 401 and stop — don't log the payload as if it were real.
  3. Deduplicate before you act. Take the event or charge identifier and write it to a table with a unique constraint, inside the same transaction as your side effect. If the insert conflicts, you've already processed this event: return 200 and do nothing. This is what makes retries harmless.
  4. Acknowledge fast, work asynchronously. Do the verification and the dedupe inline, then enqueue the heavy work — emails, fulfillment, invoicing — and return 2xx quickly. A handler that calls three internal services before responding is a handler that times out, gets retried, and creates the duplicates you just designed against.
  5. Reconcile the charge, don't trust the narrative. Before fulfilling, confirm that the charge state and amount match the order you expect. The signature proves the message is authentic; your own check proves it's the message for this order, in the state you require.
  6. Keep a backstop. Endpoints go down. Have a scheduled job that looks for orders stuck in "awaiting payment" past a reasonable window and reconciles them against the API. Webhooks are the fast path, not the only path.
  7. Rehearse in the playground, then go live. Run the full lifecycle — confirmation, expiration, overpayment, duplicate delivery — before your first real transaction. Exact header names, payload schemas and event types are in the API docs.

Conceptually, the handler is this shape:

# 1) Your backend created the charge earlier (API-key auth)
POST /charges          → { amount, asset, reference: "order_1842", expires_in }
                       ← { charge_id, payment_details, status: "pending" }

# 2) Customer pays from their own wallet; the network confirms in seconds

# 3) Payzum calls you back — signed
POST https://yourapp.com/webhooks/payzum
X-Signature: <HMAC over the raw body, using your webhook secret>
{ "charge_id": "...", "status": "confirmed", "reference": "order_1842" }

# 4) Your handler, in order:
raw      = read_raw_body(request)
expected = hmac_sha256(webhook_secret, raw)
if not constant_time_equals(expected, header_signature):
    return 401                      # never reached your order code

event = parse(raw)
if already_processed(event.id):     # unique constraint on event id
    return 200                      # retry — safe no-op

order = load_order(event.reference)
if order.amount != event.amount or event.status != "confirmed":
    flag_for_review(order); return 200

mark_paid(order); enqueue(fulfill, order)
return 200
#    Funds are already in YOUR wallet — there is no payout step.

Step 4's last line is the part that should feel strange coming from a card PSP: there is no "awaiting payout" state anywhere in the lifecycle. Settlement is the payment.

What teams actually build on signed payment webhooks

Four patterns we see repeatedly, all on the same event surface:

  • Marketplace order fulfillment. A cross-border marketplace creates one charge per order and keys webhooks to its own order reference. The verified confirmation releases the item to the seller's fulfillment queue. Because the payment is final, there's no 120-day dispute window hanging over delivered goods — and no acquirer holding the balance while buyers wait.
  • Instant digital delivery. A course platform or software vendor issues the license key, unlocks the download, or provisions the account the moment the signed webhook verifies. On fast networks that's seconds after the customer pays — and the same verified event that grants access is the one finance reconciles against. See adding crypto checkout to a store for the front half of that flow.
  • Subscription activation and renewal. A SaaS drives its own billing UI and uses webhooks to extend the entitlement window on each recurring payment. No involuntary reversals mid-cycle means no "access revoked because a dispute landed" tickets — we wrote about that dynamic in crypto subscriptions without chargebacks.
  • Multi-location and POS reconciliation. An operator running QR checkout across several locations feeds verified payment events into its own back office, keyed by terminal and cashier, so daily close matches the on-chain record instead of a bank statement that arrives two days later.
  • Agent-paid APIs. If your product is itself an API, the same account can publish an x402 endpoint in front of it: you configure your existing endpoint, API key and a price in the dashboard, Payzum returns the 402, settlement runs through an external facilitator, and the paid call is proxied to your real endpoint. AI agents pay USDC on Base per call, straight to your wallet — with no protocol work on your side.

Card-PSP callbacks vs signed webhooks for crypto payments

DimensionCard PSP / custodial gateway callbacksPayzum signed webhooks
What the event meansProvisional status in a multi-step lifecycle (auth → capture → settle)An on-chain payment already confirmed and settled
AuthenticationSigning varies by provider; some rely on IP allowlists aloneCryptographic signature over the payload, verified with your secret
Where the money is when it firesIn the provider's balance, pending a payout runIn your own wallet — settlement already happened
Can the event be reversed later?Yes — refunds, reversals, disputes up to ~120 daysNo. On-chain finality; no chargeback window exists
Duplicate deliveriesPossible — your handler must be idempotentPossible — your handler must be idempotent (same discipline, higher stakes)
Amount edge casesRare on cards; ad-hoc in most crypto gatewaysInvoice expiration + overpayment detection as platform states
Testing the failure pathsSandbox quality variesIntegration playground for the full lifecycle, signatures included
Secret handling & traceabilityVariesEncrypted secrets, 2FA, full audit log on every change

Common developer objections — answered

"Can't I just poll the API instead of exposing an endpoint?"

You can, and for low volumes it works. But polling trades a security surface for a latency-and-cost surface: you either poll rarely and make paying customers wait, or poll aggressively and burn requests on orders that haven't moved. The usual answer is both — webhooks as the fast path, a reconciliation job as the backstop for anything stuck. That's step 6 of the checklist above, and it's what makes an endpoint outage a delay instead of an incident.

"Isn't an IP allowlist or mTLS enough?"

Network controls answer "where did this come from"; a signature answers "who produced this exact payload, and was it modified". Those are different questions, and only the second one survives a proxy, a CDN, a shared egress IP, or an infrastructure change on the provider's side. Use network controls as defence in depth if you like — but the signature is the check you cannot skip.

"Payments are final — so how do refunds work?"

Finality removes involuntary reversals only. Customer service stays entirely yours: when a refund is warranted under your policy, you send funds back from your own wallet. You keep the decision and lose the dispute quota, the arbitration fees, and the "item not received" fraud on goods you already delivered.

"Do I need to run blockchain infrastructure to know a payment confirmed?"

No. Payzum handles address management, payment detection, and confirmation tracking across Bitcoin, Ethereum, Solana, Polygon, Base, Arbitrum, Optimism, BNB Chain and Avalanche. Your side of the integration is plain HTTPS: create charges with an API key, verify signatures with your webhook secret. No nodes, no RPC providers, no private keys on your application servers.

"What does this cost per transaction?"

Settlement rides on-chain network fees — cents on Base, Polygon and Solana — rather than a percentage of each sale. // confirmar pricing actual — book a call for current pricing at your expected volume.

Frequently asked questions

What are signed webhooks for crypto payments?

They are HTTP callbacks a payment processor sends to your server when a payment event occurs, carrying a cryptographic signature computed over the payload with a secret only you and the processor know. Your endpoint recomputes the signature and compares before trusting the event — so a forged or modified request can never mark an order as paid.

How do I verify a Payzum webhook signature?

Read the raw request body before any parsing, recompute the HMAC over those exact bytes using your webhook secret, and compare it to the signature header with a timing-safe comparison such as crypto.timingSafeEqual in Node or hmac.compare_digest in Python. Reject anything that doesn't match. Exact header names and payload schemas are in the Payzum API docs, and you can rehearse the whole flow in the integration playground.

Why does my signature verification keep failing?

Almost always because the body was re-serialized before you hashed it. Frameworks that parse JSON automatically can change key order, whitespace or encoding, and the signature covers exact bytes. Configure a raw-body parser on the webhook route specifically, hash that, and verify before parsing.

Do I need to handle duplicate webhook deliveries?

Yes. Any webhook transport retries after timeouts or errors, so assume at-least-once delivery. Store the event or charge identifier with a unique constraint in the same transaction as your side effect: if the insert conflicts, the event was already processed and your handler returns 200 without doing anything. With final on-chain payments there is no chargeback to undo a double fulfillment, so idempotency is not optional.

What happens if my webhook endpoint is down when a payment confirms?

The payment still settles — it is on-chain and non-custodial, so the funds arrive in your wallet regardless of whether your server answered. You recover the application state by reconciling: run a scheduled job that checks orders stuck in an unpaid state against the API and updates them. Webhooks are the fast path, not the only source of truth.

Can I test signed webhooks without moving real funds?

Yes. The integration playground lets you create charges, trigger confirmations and receive real signed webhooks end to end, including the expiration and overpayment paths, before your first live transaction — so your payment flow can be covered by automated tests like any other part of your system.

Review your webhook integration with our team

Every order model is different — what you fulfill, when you consider a payment final, how you reconcile, what your retry story looks like. Book 20 minutes with our team and we'll map your exact flow: charge creation, signature verification, idempotency, the states worth handling, and non-custodial settlement to your own wallet. No pitch — a concrete technical plan for your stack.

Prefer not to use the embed? Book directly here · [email protected]