Dispute lifecycle

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.

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.

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

StatusMeaning
needs_responseRecurrente needs supporting documentation from the merchant. This is the normal initial status.
under_reviewDocumentation was received or the case is being reviewed by Recurrente, the acquirer, or the issuer.
charge_refundedThe underlying charge was refunded during dispute handling.
closedThe case was closed without a successful representation, commonly because documentation was not received in time.
wonThe dispute was resolved in the merchant’s favor. Recurrente credits the disputed amount back to the balance.
lostThe dispute was resolved against the merchant. The original balance debit remains in place.

Webhook events

EventWhen it is sent
dispute.createA dispute record is created. The payload contains the dispute’s initial status and reason.
dispute.updateA 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

1{
2 "id": "di_ab12cd34",
3 "api_version": "2024-04-24",
4 "created_at": "2026-06-17T18:00:00Z",
5 "status": "needs_response",
6 "reason": "fraudulent",
7 "event_type": "dispute.create"
8}
FieldDescription
idStable dispute ID. It stays the same across future dispute.update events for the same dispute.
api_versionVersion of the webhook payload.
created_atWhen the dispute was created in Recurrente.
statusCurrent dispute status. Use this as the source of truth for lifecycle handling.
reasonThe reason reported by the issuer or cardholder.
event_typedispute.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:

1{
2 "id": "di_ab12cd34",
3 "api_version": "2024-04-24",
4 "created_at": "2026-06-17T18:00:00Z",
5 "status": "won",
6 "reason": "fraudulent",
7 "event_type": "dispute.update",
8 "connected": true,
9 "account_id": "ac_child123"
10}
  • connected: true indicates the event was generated by a connected account.
  • account_id is the account that generated the event.

Handler pattern

1app.post("/webhooks/recurrente", (req, res) => {
2 const event = req.body
3
4 if (event.event_type === "dispute.create" || event.event_type === "dispute.update") {
5 upsertDispute({
6 id: event.id,
7 status: event.status,
8 reason: event.reason,
9 connectedAccountId: event.account_id
10 })
11 }
12
13 res.sendStatus(200)
14})

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