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

# Dispute lifecycle

> How chargebacks move through Recurrente and which webhooks are sent

A dispute is opened when a cardholder or issuing bank challenges a paid card transaction. Recurrente links the dispute to the original invoice, debits the disputed amount from your Recurrente balance, asks for supporting documentation, and tracks the bank's resolution.

> **Note**
>
> Dispute webhooks are not unified `intent.*` events. They use their own event names: `dispute.create` and `dispute.update`.

## Typical lifecycle

1. **The issuer reports the dispute.** Recurrente receives the alert from the issuer or creates it from an issuer email when the transaction can be matched.
2. **The dispute is opened.** The status is usually `needs_response`. Recurrente debits the disputed amount from your balance, emails the account owner, and sends a `dispute.create` webhook.
3. **You send documentation.** Recurrente reviews the supporting documents and may move the dispute to `under_review`. This sends a `dispute.update` webhook.
4. **The issuer reviews the case.** The dispute stays in progress while the issuer decides. If no documentation arrives in time, Recurrente can mark the dispute `closed`.
5. **The dispute is resolved.** Recurrente updates the status to `won`, `lost`, or another final operational status. A `won` dispute credits the disputed amount back to your balance. A `lost` or `closed` dispute leaves the original debit in place.

> **Note**
>
> Not every dispute follows every step. For example, a dispute can be closed for no response, resolved directly by the issuer, or recorded as `charge_refunded` when the charge was refunded during handling.

## Statuses

| Status            | Meaning                                                                                                           |
| ----------------- | ----------------------------------------------------------------------------------------------------------------- |
| `needs_response`  | Recurrente needs supporting documentation from the merchant. This is the normal initial status.                   |
| `under_review`    | Documentation was received or the case is being reviewed by Recurrente, the acquirer, or the issuer.              |
| `charge_refunded` | The underlying charge was refunded during dispute handling.                                                       |
| `closed`          | The case was closed without a successful representation, commonly because documentation was not received in time. |
| `won`             | The dispute was resolved in the merchant's favor. Recurrente credits the disputed amount back to the balance.     |
| `lost`            | The dispute was resolved against the merchant. The original balance debit remains in place.                       |

## Webhook events

| Event            | When it is sent                                                                                         |
| ---------------- | ------------------------------------------------------------------------------------------------------- |
| `dispute.create` | A dispute record is created. The payload contains the dispute's initial `status` and `reason`.          |
| `dispute.update` | A dispute is updated. This includes status changes such as `under_review`, `closed`, `won`, and `lost`. |

`dispute.update` can be sent for saved changes that are not status changes, so always read the `status` field instead of assuming every update means the lifecycle advanced.

## 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"
}
```

| Field         | Description                                                                                      |
| ------------- | ------------------------------------------------------------------------------------------------ |
| `id`          | Stable dispute ID. It stays the same across future `dispute.update` events for the same dispute. |
| `api_version` | Version of the webhook payload.                                                                  |
| `created_at`  | When the dispute was created in Recurrente.                                                      |
| `status`      | Current dispute status. Use this as the source of truth for lifecycle handling.                  |
| `reason`      | The reason reported by the issuer or cardholder.                                                 |
| `event_type`  | `dispute.create` or `dispute.update`.                                                            |

## Reasons

The `reason` field can be one of:

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

## Connected accounts

If the dispute belongs to a connected child account and your platform account has webhooks enabled, your platform can receive the same dispute events with these extra fields:

```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` indicates the event was generated by a connected account.
* `account_id` is the account that generated the event.

## Handler pattern

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

Use the Svix `svix-id` header for delivery idempotency, and treat `event.id` as the dispute object's ID.