> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.recurrente.com/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.recurrente.com/_mcp/server.

# Migrate to Unified Webhooks (intent.*)

> How to move from per-type events to the unified intent.* format

Recurrente unified all payment events into a single format: **`intent.*`**. Instead of a separate event per payment method (`payment_intent.*`, `bank_transfer_intent.*`, etc.), you receive one `intent.*` event with a common structure, discriminated by the `type` field. You write **one** handler instead of one per type.

> **Note**
>
> **Existing accounts keep their contract.** This guide is for accounts receiving both formats that want to migrate their integration. New accounts receive only the unified event when a replacement exists; they do not need to unsubscribe from the legacy duplicate.

To recognize payments stored before migration, match `legacy_id` against the old per-type ID. Once linked, `id` and `receipt_number` identify the same payment across both formats. Keep the event type too: a later status change is not a duplicate.

## New events to configure

Subscribe your endpoint to these events from your Webhooks dashboard:

| Event              | When it's emitted                                         |
| ------------------ | --------------------------------------------------------- |
| `intent.succeeded` | Successful charge (any payment method)                    |
| `intent.pending`   | Charge started or pending (bank transfer, crypto)         |
| `intent.failed`    | Failed charge                                             |
| `intent.canceled`  | Canceled intent                                           |
| `intent.paid`      | Balance payments only: delivered to the **payer** account |

## How it works

1. For existing accounts, Recurrente still sends **both formats** for each supported payment: the legacy per-type one (`payment_intent.*`, etc.) **and** the unified one (`intent.*`), as separate deliveries.
2. In your Webhooks dashboard, subscribe your endpoint to the `intent.*` events you care about.
3. Your handler branches on the `type` field and processes.

> **Warning**
>
> If your account keeps both formats and your endpoint **doesn't filter by event type**, it will receive both `payment_intent.*` and `intent.*` for the same payment. To avoid processing the same charge twice, subscribe to **only one** of the two formats (or check the `event_type` field in your handler).

> **Note**
>
> A Sandbox lets you verify your `intent.*` handler without moving real money: a paid Sandbox checkout (card `4242 4242 4242 4242`) fires `intent.succeeded` of type `payment` to that Sandbox's endpoints. Sandboxes created before this change also receive the legacy `payment_intent.succeeded`.

## The unified payload

```json
{
  "type": "payment",            // payment | bank_transfer | crypto | balance | cash
  "event_type": "intent.succeeded",
  "id": "in_8c3a1f20",
  "status": "succeeded",        // pending | succeeded | failed | canceled | paid
  "raw_status": "succeeded",    // the concrete, non-normalized state
  "amount_in_cents": 25000,
  "currency": "GTQ",
  "customer": { "id": "cus_...", "email": "...", "nit": "CF" },
  "product": { "id": "prod_..." },
  "checkout": { "...": "..." },
  "payment":  { "...": "..." },
  "details": { "fee": 875, "installments": 1, "channel": "Tarjeta", "...": "..." }
}
```

The envelope and common core are identical for all five types; only `type` and the `details` contents change. See the full structure in the [Webhooks documentation](/guides-english/getting-started/webhooks).

## Mapping: legacy → unified

| Legacy event                                                | Unified event                        | `type`          |
| ----------------------------------------------------------- | ------------------------------------ | --------------- |
| `payment_intent.succeeded`                                  | `intent.succeeded`                   | `payment`       |
| `payment_intent.failed`                                     | `intent.failed`                      | `payment`       |
| `payment_intent.requires_capture` / `requires_verification` | `intent.pending`                     | `payment`       |
| `bank_transfer_intent.{pending,succeeded,failed}`           | `intent.{pending,succeeded,failed}`  | `bank_transfer` |
| `crypto_intent.{pending,succeeded,failed}`                  | `intent.{pending,succeeded,failed}`  | `crypto`        |
| `balance_intent.succeeded`                                  | `intent.succeeded`                   | `balance`       |
| `balance_intent.paid`                                       | `intent.paid`                        | `balance`       |
| `cash_intent.{succeeded,failed,canceled}`                   | `intent.{succeeded,failed,canceled}` | `cash`          |

Intermediate card states (`requires_capture`, `requires_verification`) are **normalized** to `pending`. The concrete state is always available in `raw_status`.

## Migration steps

1. **Review the new payload.** Look at the structure in the [Webhooks documentation](/guides-english/getting-started/webhooks), or fetch a real payment with `GET /api/intents/{id}` (returns the same format as the webhook).
2. **Update your handler** to consume `intent.*` and branch on `type`:
   ```js
   switch (event.type) {
     case "payment":       /* event.details.fee, .installments, ... */ break
     case "bank_transfer":
     case "crypto":
     case "balance":
     case "cash":          break
   }
   // state is always in event.status: pending | succeeded | failed | canceled | paid
   ```
3. **Subscribe your endpoint** to the `intent.*` events from the Webhooks dashboard.
4. **(Optional) Unsubscribe from the per-type events.** Once your unified handler works, unsubscribe from `payment_intent.*`, `bank_transfer_intent.*`, etc., to stop receiving the duplicate format.

## Events that don't change

`subscription.*`, `refund.*`, `dispute.*`, `automated_bank_transfer_intent.*`, and `setup_intent.*` stay exactly the same — they're not part of the unified `Intent` resource.