POST an event to it whenever a payment is created, starts processing, succeeds, fails, expires, is cancelled, or settles.
Webhooks vs. polling
PollingGET /v1/payments/{id}/status is the primary way to validate payment status during the purchase cycle: create the payment, then poll until the status is final. Webhooks come on top as an enhancement. They usually tell you first, and with far fewer requests, so you can update the order status in your UI, notify a customer, or kick off downstream processing the moment something happens.
When a webhook event arrives, its payload already contains everything you need. Every event is a full signed snapshot of the payment and carries a monotonic payment_state_version, so verifying the signature, deduplicating by id, and applying the version guard is enough; there is no need to call the API from inside your handler.
Webhook delivery is at-least-once and not guaranteed to be in order (see Delivery guarantees). A well-built integration works correctly even if a webhook arrives twice, late, out of order, or not at all.
Events
Every event is namedpayment.<stage>:
When you register an endpoint you choose which event types it receives; by default it receives all seven.
payment.settled carries status: "succeeded". Settlement is a stage of a succeeded payment, not a seventh status, so don’t look for a settled value in the status field.The event payload
Every event carries the same envelope, anddata is always a full snapshot of the payment at the moment the event occurred, never a delta. You never need a previous event to interpret the current one.
The fields inside
data you’ll use most:
A
payment.succeeded event looks like this:
Delivery guarantees
Design your handler around three properties of webhook delivery. Getting these right up front is the difference between an integration that works in a demo and one that works in production.At-least-once delivery
Every event is delivered at least once, which means occasionally more than once. Deduplicate on the eventid: record each id you’ve processed (with a TTL of a few days), and acknowledge but skip any event whose id you’ve already seen.
Out-of-order delivery
Events for the same payment can arrive out of order. Apayment.succeeded may land before the payment.processing that preceded it, especially when retries are involved. The version guard keeps you safe: compare payment_state_version. It increases with every state change of a payment, so keep the highest version you’ve processed per payment_id and ignore any event with a version less than or equal to it, because it is stale. Since every event is a full snapshot, a late event you process in version order can never corrupt your state.
Retries
An attempt counts as delivered only when your endpoint responds with a 2xx status within 15 seconds. Any other response, including redirects (which are not followed), or a slower one counts as a failure, and the delivery is retried with increasing back-off:
That’s roughly 28 hours of automatic retries. After the final attempt the delivery is marked failed and is not retried automatically. And if every delivery to an endpoint keeps failing for 5 days, the endpoint is disabled and stops receiving events.
Because a misconfigured URL or a disabled endpoint drops deliveries silently, don’t rely on noticing an outage. Run a periodic reconciliation job: poll
GET /v1/payments/{id}/status for any payment your records still show in a non-final state after its expires_at has passed. Use the endpoint’s delivery history in the dashboard to debug what went wrong.
To stop deliveries entirely (for example, when decommissioning an integration), delete the endpoint from the dashboard’s Webhooks page. Deletion takes effect immediately and cannot be undone.
Set up your endpoint
1
Register the endpoint
- In the dashboard, make sure the Test / Live selector matches the mode you’re setting up, since endpoints are configured separately per mode.
- Open Webhooks and add your endpoint: a publicly reachable HTTPS URL, and the event types you want (all seven by default).
- Copy the signing secret (
whsec_…) and store it in a secret manager. You’ll use it to verify every delivery.
You can register one webhook endpoint per mode. Test and live endpoints are fully separate, each with its own signing secret.
2
Verify signatures
Every delivery is signed so you can confirm it came from WalletConnect Pay and wasn’t tampered with. The signature is a standard HMAC-SHA256, so you don’t need any SDK to verify it.Two things trip people up:
Let your coding agent do it
The fastest path: an AI coding agent (Claude Code, Cursor, Codex, and the like) can implement and verify the handler in one shot. The prompt contains the complete spec plus a test vector to validate the implementation against, so paste it into your agent as-is:Or implement it by hand
The signature arrives in three headers on every delivery:Verification is four steps:
- Derive the key. Take your signing secret, strip the
whsec_prefix, and base64-decode the rest. The decoded bytes are the HMAC key (using the string itself is the most common mistake). - Compute the digest. HMAC-SHA256 over the UTF-8 bytes of
{webhook-id}.{webhook-timestamp}.{raw body}, then base64-encode it. - Compare. The delivery is authentic if your digest equals any
v1entry inwebhook-signature, using a constant-time comparison. - Check the timestamp. Reject deliveries whose
webhook-timestampis more than 5 minutes from the current time, to prevent replay attacks.
- Verify the raw request body, byte for byte, before any body-parsing middleware runs. Parsing and re-serializing the JSON changes the bytes (even a whitespace difference) and the verification fails.
- The timestamp check depends on your server clock being accurate.
400, and never process an unverified payload. The scheme is the one implemented by Svix libraries (they accept the webhook-* header names too), so if you already use one, its verify function performs exactly these checks and is equally fine.3
Test the delivery
Two ways to see events hit your endpoint before any real payment exists:
- Send a test event. In the dashboard’s Webhooks page, send a test event of any type to your endpoint. Test events carry sample data with
live: false; use them to check signature verification and your 2xx response. - Run the full flow in test mode. Create a test payment and drive it through its lifecycle with the Test Console or the dashboard’s simulation actions. Each transition emits the real webhook, from
payment.createdthroughpayment.succeeded(orfailed,expired,cancelled), so you can verify your handler against every path, including the ones that are hard to produce on demand with real payments.
Go live
Webhook configuration does not carry over from test to live; they are separate environments end to end. When you switch:- Flip the dashboard to Live and register your production endpoint URL and event types again.
- Store the live signing secret, which is different from your test secret, and make sure your handler uses the secret matching the environment.
- Confirm your handler checks
data.live === truebefore triggering real fulfillment. Test events and test-mode payments always carrylive: false. - Re-verify the safety rails: dedupe by
id, ordering bypayment_state_version.
Next steps
Webhook event reference
Every field of every event type, with types, nullability, and examples.
Test mode
Simulate every payment status transition and watch the webhooks arrive.
Get the payment status
The polling endpoint to confirm state on the critical path.