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

# Ciclo de vida de contracargos

> Cómo avanzan los contracargos en Recurrente y qué webhooks se envían

Un contracargo se abre cuando el tarjetahabiente o el banco emisor disputa una transacción de tarjeta pagada. Recurrente vincula el contracargo a la factura original, descuenta el monto disputado del balance de Recurrente, solicita documentación de soporte y registra la resolución del banco.

> **Note**
>
> Los webhooks de contracargos no son eventos unificados `intent.*`. Usan sus propios eventos: `dispute.create` y `dispute.update`.

## Ciclo típico

1. **El emisor reporta el contracargo.** Recurrente recibe la alerta del emisor o la crea desde un correo del emisor cuando logra encontrar la transacción.
2. **Se abre el contracargo.** El estado normalmente es `needs_response`. Recurrente debita el monto disputado del balance, envía un correo al propietario de la cuenta y envía un webhook `dispute.create`.
3. **Envías documentación.** Recurrente revisa los documentos de soporte y puede mover el contracargo a `under_review`. Esto envía un webhook `dispute.update`.
4. **El emisor revisa el caso.** El contracargo sigue en progreso mientras el emisor decide. Si no llega documentación a tiempo, Recurrente puede marcar el contracargo como `closed`.
5. **El contracargo se resuelve.** Recurrente actualiza el estado a `won`, `lost` u otro estado operativo final. Un contracargo `won` acredita el monto disputado de vuelta al balance. Un contracargo `lost` o `closed` deja el débito original en firme.

> **Note**
>
> No todos los contracargos pasan por todos los pasos. Por ejemplo, un contracargo puede cerrarse por falta de respuesta, resolverse directamente por el emisor o registrarse como `charge_refunded` cuando el cargo fue reembolsado durante el manejo del caso.

## Estados

| Estado            | Significado                                                                                                   |
| ----------------- | ------------------------------------------------------------------------------------------------------------- |
| `needs_response`  | Recurrente necesita documentación de soporte del comercio. Es el estado inicial normal.                       |
| `under_review`    | Recurrente recibió documentación o el caso está en revisión por Recurrente, el adquirente o el emisor.        |
| `charge_refunded` | El cargo original fue reembolsado durante el manejo del contracargo.                                          |
| `closed`          | El caso se cerró sin una representación exitosa, comúnmente porque no se recibió documentación a tiempo.      |
| `won`             | El contracargo se resolvió a favor del comercio. Recurrente acredita el monto disputado de vuelta al balance. |
| `lost`            | El contracargo se resolvió en contra del comercio. El débito original al balance queda en firme.              |

## Webhooks

| Evento           | Cuándo se envía                                                                                            |
| ---------------- | ---------------------------------------------------------------------------------------------------------- |
| `dispute.create` | Se crea un contracargo. El payload incluye el `status` y `reason` iniciales.                               |
| `dispute.update` | Se actualiza un contracargo. Esto incluye cambios de estado como `under_review`, `closed`, `won` y `lost`. |

`dispute.update` también puede enviarse por cambios guardados que no son cambios de estado, así que lee siempre el campo `status` en vez de asumir que cada update avanzó el ciclo.

## Payload

```json
{
  "id": "di_ab12cd34",
  "api_version": "2024-04-24",
  "created_at": "2026-06-17T18:00:00Z",
  "status": "needs_response",
  "reason": "fraudulent",
  "event_type": "dispute.create"
}
```

| Campo         | Descripción                                                                                              |
| ------------- | -------------------------------------------------------------------------------------------------------- |
| `id`          | ID estable del contracargo. Se mantiene igual en futuros eventos `dispute.update` del mismo contracargo. |
| `api_version` | Versión del payload webhook.                                                                             |
| `created_at`  | Fecha en que se creó el contracargo en Recurrente.                                                       |
| `status`      | Estado actual del contracargo. Úsalo como fuente de verdad para manejar el ciclo de vida.                |
| `reason`      | Razón reportada por el emisor o tarjetahabiente.                                                         |
| `event_type`  | `dispute.create` o `dispute.update`.                                                                     |

## Razones

El campo `reason` puede ser uno de:

`bank_cannot_process`, `check_returned`, `credit_not_processed`, `customer_initiated`, `debit_not_authorized`, `duplicate`, `fraudulent`, `general`, `incorrect_account_details`, `incorrect_amount`, `insufficient_funds`, `product_not_received`, `product_unacceptable`, `subscription_canceled`, `unrecognized`.

## Cuentas conectadas

Si el contracargo pertenece a una cuenta hija conectada y tu cuenta plataforma tiene webhooks activos, tu plataforma puede recibir los mismos eventos con estos campos adicionales:

```json
{
  "id": "di_ab12cd34",
  "api_version": "2024-04-24",
  "created_at": "2026-06-17T18:00:00Z",
  "status": "won",
  "reason": "fraudulent",
  "event_type": "dispute.update",
  "connected": true,
  "account_id": "ac_child123"
}
```

* `connected: true` indica que el evento fue generado por una cuenta conectada.
* `account_id` es la cuenta que generó el evento.

## Patrón para tu handler

```js
app.post("/webhooks/recurrente", (req, res) => {
  const event = req.body

  if (event.event_type === "dispute.create" || event.event_type === "dispute.update") {
    upsertDispute({
      id: event.id,
      status: event.status,
      reason: event.reason,
      connectedAccountId: event.account_id
    })
  }

  res.sendStatus(200)
})
```

Usa el header `svix-id` para idempotencia de entrega, y trata `event.id` como el ID del objeto de contracargo.