Skip to Content
WebhooksOverview
View as Markdown

Webhooks — Overview

Webhooks are the authoritative signal. Browser callbacks (onSuccess) and dashboard views are convenience; webhooks are ground truth.

Delivery guarantees

  • At least once. A single event may be delivered up to 6 times if your server doesn’t return 2xx within the timeout. Make your handler idempotent — dedup on X-Delivery (the payload has no event_id field; the stable delivery UUID is the idempotency key).
  • One event per HTTP request. No batching.
  • Per-endpoint isolation. If you have multiple registered endpoints, each gets its own delivery and retry track. One slow endpoint doesn’t delay the others.
  • Signed. Every payload carries an X-Signature header (and during the 24-hour window after a rotation, also an X-Signature-Prev). Verify before doing anything with the body. See Signature verification.

Subscribable event types

EventFires when…
payment.settledThe on-chain transfer cleared the chain’s confirmation count. Use this to mark orders paid.
payment.failedA fiat payment was rejected by the payment provider. Not fired for crypto timeouts — those surface as checkout.expired instead, and short crypto payments surface as payment.underpaid.
payment.underpaidFunds arrived but short of the order total (typical: stablecoin transfer fee taken from the amount).
payment.overpaidFunds arrived in excess of the order total. The surplus is recorded but not auto-refunded.
order.createdA new order was opened — either by your B2B API call or by a checkout-session conversion.
order.canceledAn order moved to cancelled. The payload’s data.reason distinguishes manual cancel from payment_timeout (an unpaid order timed out).
order.resolvedA PARTIAL_PAID order was resolved to PAID — the merchant accepted the shortfall.
order.reopenedA previously auto-canceled order (canceled_reason=payment_timeout) was reopened by the merchant.
checkout.createdA buyer opened the checkout for an order.
checkout.completedThe buyer-side flow finished (does not imply on-chain settlement — use payment.settled for that).
checkout.expiredThe buyer abandoned and the session TTL ran out.
payment.refund.requestedA refund record was created — either from a merchant-initiated API call or from a customer-submitted refund-request form.
payment.refund.approvedA pending refund passed your approval workflow.
payment.refund.rejectedA pending refund was denied.
payment.refund.executedThe refund’s on-chain transfer cleared and the record moved to terminal executed.
refund_request.createdA refund-request token was minted. data.source is b2b / dashboard / renewal. Optional to subscribe — useful for audit pipelines that track which token is currently active per order.
refund_request.renewal_requestedA buyer clicked “Request new link” after their token expired. Strongly recommended to subscribe — this is the merchant’s cue that the renewal widget has a new item to act on.
refund_request.renewedA renewal was approved and a new token replaced the old one. data.old_token / data.new_token form the audit chain.
refund_request.canceledA merchant flipped a token to CANCELED from the dashboard (e.g. declined a renewal request, killed a live link). Idempotent — only the first transition emits. data.reason is the optional merchant note.

Planned (Coming soon)

Coming soon. These events belong to recurring invoices and subscriptions, which are not yet available. They are not in the subscribable table above and cannot be subscribed to today. See Recurring invoices.

Planned eventFires when…
subscription.createdA subscription is created.
invoice.createdAn invoice for a billing cycle is created.
invoice.paidAn invoice is paid.
subscription.past_dueAn invoice is unpaid past its due date.
subscription.canceledA subscription is canceled.

The dashboard’s endpoint form lists the same events. Subscribing to an event that doesn’t exist is rejected when you save the endpoint.

Test events aren’t subscribable. The dashboard’s per-endpoint Send Test button sends a webhook.test.ping event to that one endpoint immediately, without retries. It isn’t in the catalog above: you receive it because you have a registered endpoint, not because you subscribed.

Subscribe to only the events you handle. Each endpoint has its own event filter; the wildcard "*" means “every event, including ones added in the future”. Subscribing to fewer events keeps your handler simpler and means fewer retries when your endpoint has errors.

Payload + headers

The HTTP body is the event-specific data object directly. No outer Stripe-style envelope — fields like the event type, delivery ID, and emission timestamp live in headers instead. For payment.settled the body looks like:

{ "receipt_id": "rcp_…", "order_id": "ord_…", "payment_intent_id": "pin_…", "checkout_session_id": "cst_…", "merchant_id": "mer_…", "customer_id": "cus_…", "total": "49.00", "currency": "USD", "payment_method": "crypto", "token": "USDC", "network": "polygon", "tx_hash": "0x…", "deposit_address": "0x…", "treasury_address": "0x…", "amount_received": "49.00", "confirmations": 5, "metadata": { /* per-event */ } }

Other events carry their own fields. Field names are stable (lower snake_case); the on-chain transaction hash is always tx_hash.

tx_hash is the transaction identifier in the network’s own format (0x… on EVM chains; the native hash or signature on TRON, Solana and TON). deposit_address can be absent for TRON, Solana and TON, because buyers pay your treasury wallet directly there, and confirmations follows Chains & assets.

Headers on the inbound request

Content-Type: application/json X-Event: payment.settled X-Delivery: 7c9e6679-7425-40de-944b-e07fc1f90ae7 Idempotency-Key: 7c9e6679-7425-40de-944b-e07fc1f90ae7 X-Timestamp: 1729536000 X-Signature: sha256=9a8b7c… X-Signature-Prev: sha256=fa31b2… (only during a rotation grace window)
HeaderWhat it is
X-EventThe event type (e.g. payment.settled). Route on this at the proxy layer if you want to skip JSON parsing.
X-DeliveryUUID identifying the delivery row. Stable across all retries of the same (event, endpoint) pair — use it as your idempotency key.
Idempotency-KeyMirrors X-Delivery (same value). Set on every delivery.
X-TimestampUnix-seconds when the attempt was sent. Signed into the payload so a captured (body, X-Signature) pair can’t be replayed indefinitely — reject deliveries whose timestamp is outside your tolerance window.
X-Signaturesha256=<hex> of HMAC-SHA256(secret, X-Timestamp + "." + raw_body). See Signature verification.
X-Signature-PrevSame algorithm with the previous secret. Present only in the 24-hour window after you rotate — lets verifiers running either key keep accepting deliveries during the cutover. After the window closes the header stops being sent.

Retry schedule

If your endpoint doesn’t return 2xx within the timeout, we retry on this schedule (timestamps relative to first attempt):

AttemptDelayCumulative
10s0s
2+1 min1m
3+5 min6m
4+15 min21m
5+1 hour1h 21m
6+6 hours7h 21m

After attempt 6 fails, the delivery is marked Failed and your account email is notified. You can replay failed events from the dashboard’s Developers → Webhooks → Delivery history panel. Each replay is a new delivery with its own X-Delivery.

Register an endpoint

From the merchant dashboard :

  1. Developers → Webhooks → + Add endpoint
  2. Paste your URL — https://… only (plain HTTP is rejected; the create form also blocks localhost, private IP ranges, and URLs carrying userinfo)
  3. Choose events to subscribe (or * for all)
  4. Pick environment — test or live (each gets its own secret; they never cross over)
  5. Save → the dashboard displays the signing secret (whsec_…) once. Store it server-side; you’ll need it for the next two features.

You can register up to 10 endpoints per environment per merchant (e.g., one for production fulfillment, one for staging mirroring, one for a Slack notifier). Each has its own retry state and secret.

Lifecycle actions on each endpoint

The ⋮ menu on each endpoint card surfaces:

  • Edit — change the URL, description, or subscription list. The new URL is re-validated with the same https:///SSRF rules as create.
  • Send Test — synchronously POSTs a webhook.test.ping envelope signed with your current secret. The dashboard shows the HTTP status, latency, and a 512-byte snippet of your response. Test pings aren’t retried, so you get an immediate answer.
  • Rotate Secret — generates a new secret. The previous one stays valid for 24 hours (deliveries carry both X-Signature and X-Signature-Prev during the window so verifiers running either key keep accepting events while you redeploy).
  • Reveal Secret — re-displays the existing secret. Gated by fresh 2FA verification and recorded in the audit log; use it only when you lost your copy and Rotate isn’t acceptable.
  • Enable / Disable — switch the endpoint on or off without losing delivery history. Disabled endpoints stay in the dashboard but receive no new deliveries.
  • Delete — permanent. Use Disable if you might re-enable later.

Tips for handlers

  1. Return 2xx fast. Acknowledge with 200 OK before doing heavy work — move fulfillment to a background job. The per-attempt timeout is 10 seconds; holding the response longer than that triggers a retry. The timeout is platform-side and not merchant-configurable — contact support if your handler genuinely needs more time.
  2. Dedup on X-Delivery (or Idempotency-Key — same value). Even if you return 2xx, an upstream proxy could drop the connection and trigger a retry; the delivery ID is stable across every retry of the same delivery row, so it’s the right key.
  3. Tolerate unknown event types. New events may appear; return 200 and no-op rather than 4xx, or those deliveries will keep retrying.
  4. Log X-Delivery next to your business logic. When something goes wrong, that’s the join key between our side and yours.

What’s next