Skip to main content

Lifecycle

Webhooks for both ways of paying are named pool.*. This is the stream your integration receives. Release goods on capture: pool.deposit.confirmed on the deposit rail, pool.swept on the connect rail. Both fire when the customer’s transfer has reached the required confirmation depth, which is the point after which the money can only reach your pool. The one exception is payer screening set to Enforce — the sender is screened at sweep time, so on the deposit rail wait for pool.swept instead. Finality. Two things about that wait your customers will ask you about. The checkout shows no per-confirmation progress: it says only that the payment is confirming, never “3 of 20”, so against the required confirmation depths it is a long stretch watching something that doesn’t move. And it runs two separate countdowns — the rate lock, and how long the payment address stays valid — whose copy reads like one clock, so customers ask why the timers disagree. Both are known rough edges. Roadmap.
Older integrations may still reference a payment_* event stream (payment_intent_created, payment_submitted, payment_confirmed, payment_failed, payment_expired). Those belong to a retired checkout flow that the current widget and SDK no longer use, and they do not fire for payments taken through the pool. Build against the pool.* events below.

Envelope

Every delivery has the same shape. id is the idempotency key — the same event retried carries the same id, so you can dedupe on it.

Events

pool.swept — settled in your pool

The money is in your pool and the payment is irreversible.Connect rail: the customer’s deposit transaction reached required confirmations. That is this rail’s capture and the only event it emits, so this is where you fulfil. Deposit rail: our sweep of the invoice address into your pool confirmed, after the earlier pool.deposit.confirmed that already captured the payment — here it is a settlement receipt, and it is your fulfilment trigger only if you run payer screening set to Enforce.
There is no flat amount field. expectedAmount and confirmedAmount are both token base units. USDC, USDT and EURC use 6 decimals on every chain we settle on today, so 49000000 is 49.00. Scale by the token’s own decimals anyway rather than hard-coding 6 — a token added later need not match.
Deposit rail only. The customer’s transfer reached the required confirmation depth at the invoice address. This is your fulfilment trigger on this rail. The invoice address is sweep-only — its return-to-sender path is closed from the moment the address is created, it has no second destination, and the pool it pays into is fixed inside the address itself — so a confirmed deposit can only reach your pool. The sweep that follows, pool.swept, decides when the money is available there, not whether it arrives.Same data shape as pool.swept, with status reading paid, overpaid, or expired_paid_late.The one exception is payer screening set to Enforce: there the sender is screened at sweep time, so treat this event as captured-but-not-final and fulfil on pool.swept instead. The checkout the customer sees follows the same rule — it declares the payment complete at this point, except in Enforce mode, where it keeps showing “confirming” until the sweep confirms. The confirmAtCapture field on the status API tells you which case your account is in.
A distribute() transaction confirmed and the split was credited to the committed recipients. Use this for payout reconciliation.At this point your share is claimable inside the pool — the later claim() that moves it to your payout address emits no webhook, so reconcile from this txHash plus the on-chain claim. Claiming.
Deposit rail only, and it is never sent today. We send it only in response to a return-to-sender on an invoice address, and that path is closed from the moment the address is created: every address we issue is created with a zero-length return window, so the return can never be called and an inbound deposit can only ever move forward into your pool. The event name stays in the schema and in the handler example below, so a case for it is harmless — but nothing can produce one. If you ever receive one, treat the order as unpaid and contact support.This is not how you refund a customer. Refunds are sent by you, from your own wallet, to an address the customer supplies — never back to the address the payment came from. Refunds.
Our indexer independently re-derived the pool and invoice addresses for this payment and got a different answer from the one on record, so capture was halted.This is a terminal hold. Do not ship the order. It clears only by manual review — contact support. Verification.

Things that do not send a webhook

Poll the status API for these — no callback arrives.
  • Intent creation. Nothing is delivered when a customer starts checkout.
  • Broadcast-but-not-yet-final. There is no “confirming” event; the first webhook is pool.deposit.confirmed or pool.swept.
  • Expiry. payment_expired exists as a name in our schema but nothing emits it. An expired quote shows up only as the expired status. Don’t cancel the order irreversibly on it — expiry locks the price, not the address, and a late payment can still arrive.
  • Blocked payments. When payer screening refuses a payment, the checkout gets a 403 with error_code: payment_blocked synchronously — no webhook. If a sanctioned sender’s funds arrive at an already-issued invoice address, the sweep is frozen and the payment moves to held_sanctioned, also with no webhook. Errors · Sanctions.
  • Underpayment and overpayment. These are statuses, not events. pool.deposit.confirmed fires with status: "overpaid"; an underpayment fires nothing until it is resolved.

Handling them

Ignore events you don’t recognise rather than erroring. New event types can be added, and a handler that 500s on an unknown type will look like an outage to our retry logic.

Deposit-rail statuses

The deposit rail carries richer per-invoice states than the events above. All twelve surface in your dashboard and the status API: Treat any status you don’t recognise as unsettled rather than shippable.
An invoice address is only watched for a limited window: seven days from when the payment was created, by default, and configurable down to one hour. A payment that lands inside that window is still credited, as expired_paid_late. A payment that lands after it isn’t detected automatically — the funds are safe, because that address can only pay your pool, but recovering them needs us to step in, so contact support. Mark the order lapsed rather than cancelled, and don’t wait indefinitely.
What to do about each.