Skip to main content
Webhooks are how your server learns that money arrived. Fulfil orders from them, not from anything that happens in the customer’s browser.
Browser callbacks fire only if the customer’s tab is still open. Webhooks do not depend on it. Fulfil from webhooks.
Delivery is best-effort, not at-least-once. A delivery record is written only after the first send is attempted, so if the internal hand-off to our delivery service fails, the event is dropped silently — it never appears in the Webhooks tab and the retry job has nothing to recover. Treat webhooks as the fast path, not the ledger: reconcile against Payments or the payment status endpoint before concluding that a payment never arrived.

Set up

Build an endpoint

A public HTTPS URL that accepts POST and responds 2xx quickly.

Register it

Settings → Webhooks in the dashboard. Add the URL.Then reveal your signing secret — once — and store it where your server reads it. If you lose it, rotate it on the same page.

Verify every delivery

Reject anything that fails. Signatures.

Test it

Use Send test payload on the Webhooks tab, then make a real testnet payment.The test is a real signed delivery — same header, same envelope — with event payment.test and data.test: true. If your handler verifies it, it verifies production.
Localhost, private IP ranges, and cloud metadata addresses are rejected. Use a tunnel like ngrok for local development.

The payload

Every delivery has the same envelope, including the dashboard’s test send.
string
Stable per transition. Use it as your idempotency key.
string
The event type — pool.deposit.confirmed, pool.swept, pool.distributed, pool.refunded, or pool.tamper_suspected. Fulfil at capture: pool.deposit.confirmed when the customer sent a plain transfer, pool.swept when they paid from a connected wallet. Full list.
string
ISO 8601 timestamp.
object
The payment. orderId is the id you passed to updateCart; match on it to find the order. Amounts are token base units as strings (6 decimals for USDC, USDT and EURC). EVM and TRON deliveries carry the same fields; pool.distributed describes a payout rather than one payment, so it carries pool fields instead.

Headers

Every delivery carries these headers, including the dashboard’s test send.

A correct handler

Three rules in that snippet, all of which matter:

Raw body

Parsing and re-serialising changes the bytes and breaks the signature.

Verify first

Before parsing, before touching your database.

Acknowledge fast

We time out at 10 seconds. Do slow work in a queue.

Signature verification in detail

With replay protection and language examples.