Migrate to Unified Webhooks (intent.*)

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.

No breaking changes. Recurrente sends both formats: the classic per-type one and the unified one. Your current integration keeps working untouched; you choose which to subscribe your endpoint to, and migrate whenever you want.

New events to configure

Subscribe your endpoint to these events from your Webhooks dashboard:

EventWhen it’s emitted
intent.succeededSuccessful charge (any payment method)
intent.pendingCharge started or pending (bank transfer, crypto)
intent.failedFailed charge
intent.canceledCanceled intent
intent.paidBalance payments only: delivered to the payer account

How it works

  1. Recurrente sends both formats for each 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.

If 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).

The unified payload

1{
2 "type": "payment", // payment | bank_transfer | crypto | balance | cash
3 "event_type": "intent.succeeded",
4 "id": "in_8c3a1f20",
5 "status": "succeeded", // pending | succeeded | failed | canceled | paid
6 "raw_status": "succeeded", // the concrete, non-normalized state
7 "amount_in_cents": 25000,
8 "currency": "GTQ",
9 "customer": { "id": "cus_...", "email": "...", "nit": "CF" },
10 "product": { "id": "prod_..." },
11 "checkout": { "...": "..." },
12 "payment": { "...": "..." },
13 "details": { "fee": 875, "installments": 1, "channel": "Tarjeta", "...": "..." }
14}

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.

Mapping: legacy → unified

Legacy eventUnified eventtype
payment_intent.succeededintent.succeededpayment
payment_intent.failedintent.failedpayment
payment_intent.requires_capture / requires_verificationintent.pendingpayment
bank_transfer_intent.{pending,succeeded,failed}intent.{pending,succeeded,failed}bank_transfer
crypto_intent.{pending,succeeded,failed}intent.{pending,succeeded,failed}crypto
balance_intent.succeededintent.succeededbalance
balance_intent.paidintent.paidbalance
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, 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:
    1switch (event.type) {
    2 case "payment": /* event.details.fee, .installments, ... */ break
    3 case "bank_transfer":
    4 case "crypto":
    5 case "balance":
    6 case "cash": break
    7}
    8// 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.*, and setup_intent.* stay exactly the same — they’re not part of the unified Intent resource.