Skip to main content

The contract

Duplicates are possible and so are drops. A retried event arrives again, so idempotency is your responsibility — but delivery is not guaranteed either: the delivery record is written only after the first send is attempted, so an event whose internal hand-off fails is dropped silently, never appears in the Webhooks tab, and is never retried. Reconcile against Payments or the status API before concluding a payment never arrived. Overview.

Acknowledge fast

Ten seconds is the whole budget. Do the minimum synchronously and queue the rest.
Anti-pattern to avoid: calling a payment provider, generating a PDF, or sending an email inline. Any of those can exceed the timeout, which turns a successful delivery into a retry and a duplicate.

Idempotency

Every real event delivery carries a stable X-Webhook-Id, which also appears as id in the body. Retries of the same event reuse it.
Record the ID and do the work in the same transaction. Otherwise a crash between them leaves you having recorded an event you never acted on.
The dashboard’s Send test payload button sends a real signed delivery with its own X-Webhook-Id, so it exercises your signature check and your de-duplication. It is a single send, though — it will not show you a retry.

When your endpoint is down

A failed delivery is retried on a fixed schedule — after 1 minute, 5 minutes, 30 minutes, 2 hours, then 12 hours — for a maximum of 6 attempts, about 14.5 hours from the first try. If you’re down for a deploy, deliveries resume once you’re back. After the sixth failed attempt the delivery is dead-lettered: it is never retried again, and there is no way to replay it from the dashboard. It stays visible in the Webhooks tab so you can see what was missed. An outage shorter than that window costs you nothing; an outage longer than it means those events never arrive, which is why the reconciliation below is not optional.
Funds are not at risk here: webhooks are notifications, not settlement, and money sent to a one-time pay-to address can only ever move into your pool. But silence is not proof of settlement. Some outcomes emit no webhook at all — underpaid, wrong_token, and, if you run payer screening in enforce mode, held_sanctioned. In each of those the funds are still sitting at the pay-to address rather than in your pool, and each needs action from you; see Edge cases. They do appear as rows in Payments and in the status API. The one case that appears nowhere is a payment arriving at an address whose order has already settled — we stop watching it, so there is no row and no webhook. Coming A dedicated Unsettled view is designed and not yet built; see the Roadmap.

Reconciliation

Don’t rely on webhooks alone. Two backstops worth building:

Poll for stragglers

For orders still pending after a sensible window, call the status API with the intent’s depositIntentId.

Reconcile periodically

Compare your paid orders against the dashboard’s ledger. Any gap is a webhook you missed.
The status API is gated on Origin, not on your API key. POST /pool-deposit-status returns 403 Origin not allowed for this merchant unless the request carries an Origin header matching one of your allowed origins — and a request with no Origin header at all is refused the same way. A server-side poller must therefore send an Origin you have registered, so add your server’s origin alongside your storefront’s before you build this backstop. See API introduction.

Debugging

The dashboard’s Webhooks tab shows recent deliveries with response codes and bodies. Start there — most failures are a signature mismatch caused by a parsed body, or a timeout from inline work.

Signature verification

The most common source of rejected deliveries.