# Webhooks

Receive real-time events when checkout charges are created, 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.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 three events. If you have an **older** endpoint created before `checkout.payment.failed` existed, its subscription is unchanged — add that event type to the endpoint to start receiving failures. The [transaction-status reads](/docs/checkout-transactions) 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.

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`):

```json
{
  "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`](/docs/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:

```json
{
  "id": "evt_01HXRB...",
  "type": "checkout.payment.completed",
  "createdAt": "2026-07-22T12:01:20.000Z",
  "livemode": true,
  "data": {
    "id": "chk_124",
    "externalId": "user_42",
    "amount": "39.92",
    "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 a discounted session, `data.amount` is the final payable total and `data.pricing` preserves the original amount, normalized percentage, discount amount, and final 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 body `id`).
- `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 `2xx` quickly, then process the event asynchronously.
- `checkout.payment.completed` is retried up to three total attempts on timeouts, network errors, or `5xx` responses (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` (or `Dolafy-Delivery-Id`), and reconcile checkout status with `GET /v1/checkout/transactions/:transactionReference`.
- Fulfil only after `checkout.payment.completed`; treat `checkout.payment.failed` as 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.
