Webhooks
Receive real-time events when checkout charges are created, underpaid, paid, or fail.
Checkout webhooks notify your server when a hosted or API-created charge changes state. Register endpoints from your dashboard under Developers → Checkout.
Endpoint scope
Each webhook endpoint is product-scoped. An endpoint registered under Developers → Checkout receives only checkout events — it never receives banking or card events. Each endpoint has its own verification token.
Checkout endpoints can additionally filter by product: subscribe to all products or to a selected set. A product-scoped endpoint only receives events for its selected products.
Event types
| Event | When |
|---|---|
checkout.initiated |
A hosted or API-created checkout charge is created. |
checkout.payment.underpaid |
A valid stablecoin deposit was received, but the cumulative amount is still below the charge amount. Never fulfil from this event. |
checkout.payment.completed |
Checkout detects a successful payment (data.status is paid). |
checkout.payment.failed |
A PIX checkout reaches a terminal failed or expired state. Never fulfil from this event. |
New checkout endpoints subscribe to all four events. Existing Checkout endpoints were automatically subscribed to checkout.payment.underpaid during its rollout without changing their product filters. If you have an older endpoint created before checkout.payment.failed existed, add that event type to start receiving PIX failures. The transaction-status reads report terminal failure regardless of your webhook subscription.
Payload
Every delivery wraps the charge in a common envelope. Branch on data.paymentMethod: PIX uses bank / pix, BRL amounts, and null on-chain fields; stablecoins use crypto with chain and address details. A successful payment always reports data.status of paid, regardless of rail.
For checkout.payment.underpaid, data.amountReceived is cumulative,
data.amountRemaining is the unpaid remainder, and data.receipt identifies the individual deposit. Each
partial deposit gets its own event. A deposit that brings the cumulative total to or above the
charge amount emits checkout.payment.completed instead.
{
"id": "evt_01HXRU...",
"type": "checkout.payment.underpaid",
"createdAt": "2026-08-26T17:44:42.000Z",
"livemode": true,
"data": {
"id": "chk_125",
"amount": "3.400000",
"amountReceived": "3.100000",
"amountRemaining": "0.300000",
"paymentMethod": "crypto",
"settlementCurrency": "USDC",
"chain": "solana",
"status": "underpaid",
"receipt": {
"amount": "3.100000",
"currency": "USDC",
"chain": "solana",
"txSignature": "2EysQdNzKJXw...",
"receivedAt": "2026-08-26T17:44:40.000Z"
}
}
}
This example is a hosted-link payment — the buyer paid through the product’s checkoutUrl, so data.urlParams carries whatever query parameters you appended to the link (such as utm_source):
{
"id": "evt_01HXR9...",
"type": "checkout.payment.completed",
"createdAt": "2026-07-21T12:01:20.000Z",
"livemode": true,
"data": {
"id": "chk_123",
"checkoutProductId": "6a713a92-350a-4cc6-a996-1e55e97d3a36",
"product": { "id": "6a713a92-350a-4cc6-a996-1e55e97d3a36", "slug": "c-8f41a2d9", "name": "Curso Pro" },
"externalId": "order_1048",
"buyerEmail": "ana.silva@example.com",
"amount": "49.90",
"amountReceived": "49.90",
"pricing": null,
"currency": "BRL",
"paymentMethod": "bank",
"paymentRail": "pix",
"status": "paid",
"createdAt": "2026-07-21T12:00:00.000Z",
"updatedAt": "2026-07-21T12:01:20.000Z",
"urlParams": { "userID": "order_1048", "utm_source": "ads" }
}
}
API-created sessions
For a charge created with POST /v1/checkout/sessions, data.externalId is the userId you sent, and data.urlParams always includes your supplied or generated reference as txn_id — match on it to find your order:
{
"id": "evt_01HXRB...",
"type": "checkout.payment.completed",
"createdAt": "2026-07-22T12:01:20.000Z",
"livemode": true,
"data": {
"id": "chk_124",
"externalId": "user_42",
"amount": "37.45",
"pricing": {
"originalAmount": "29.00",
"discountPercent": null,
"discountAmount": "0.00",
"finalAmount": "37.45",
"currency": "USD"
},
"status": "paid",
"urlParams": {
"txn_id": "txn_1048",
"userID": "user_42",
"userEmail": "ana.silva@example.com"
}
}
}
…the other data fields are the same as in the hosted example above. For an API-created session, data.amount is the final payable total and data.pricing preserves the original catalog amount and final amount. Discounted pricing also includes its normalized percentage and discount amount. Charges without session pricing metadata return pricing: null.
Verifying deliveries
Every request includes these headers:
X-Dolafy-Token— your per-endpoint verification token, shown once when the endpoint is created.Dolafy-Event-Id— unique event id (also the bodyid).Dolafy-Event-Type— the event type.Dolafy-Delivery-Id— unique delivery id; retries of the same delivery reuse it.
Compare X-Dolafy-Token to the token you stored at creation and reject the request if it does not match. There is no HMAC signature — because the token is sent on every request, your endpoint must use HTTPS, keep the token secret, and avoid logging request headers. Rotate the token by recreating the endpoint.
Delivery semantics
- Respond with a
2xxquickly, then process the event asynchronously. checkout.payment.completedis retried up to three total attempts on timeouts, network errors, or5xxresponses (backoff 500ms then 1s), reusing the same event and delivery ids. All other checkout events are single-attempt.- There is no durable retry queue or replay endpoint. Make your handlers idempotent by
Dolafy-Event-Id(orDolafy-Delivery-Id), and reconcile checkout status withGET /v1/checkout/transactions/:transactionReference. - Fulfil only after
checkout.payment.completed; treatcheckout.payment.underpaidas informational andcheckout.payment.failedas terminal failure/expiry. - Manual test deliveries sent from the dashboard include top-level
"test": true. Never fulfil an order or move money from a test event.