# Disputas

Consulta las disputas (chargebacks) de tu cuenta, el detalle con la evidencia cargada y responde una disputa con evidencia.

> [!WARNING]
> El SDK `vexor` de npm (Stripe, MercadoPago, PayPal, Square, Talo) está **deprecado**: ya no se mantiene y no debe usarse. Vexorpay es hoy una **API REST sobre Stripe, siempre en USD, sin SDK oficial**. Migración: <https://developers.vexorpay.com/docs/migracion-desde-el-sdk>

Fuente: <https://developers.vexorpay.com/docs/api/disputes>

Las disputas (`dispute`, o *chargeback*) son reclamos que abre el titular de una tarjeta. Podés listarlas, ver el detalle con el estado en vivo de Stripe y responder con evidencia antes del plazo.

> **La aceptación de disputas no está disponible por API.** Aceptar (perder) una disputa es irreversible y sólo puede hacerse desde el [dashboard](/dashboard/disputes). Esta API sólo permite listar, ver y responder.

## Listar disputas

`GET /api/v1/disputes`

### Parámetros

| Parámetro | Descripción |
| --- | --- |
| `status` | `warning_needs_response`, `warning_under_review`, `warning_closed`, `needs_response`, `under_review`, `won`, `lost`, `expired` o `closed`. |
| `charge_id` | Filtrar por el cobro original asociado a la disputa. |
| `sandbox` | `true` (solo disputas de prueba) o `false` (solo producción). Sin el parámetro devuelve ambos entornos. |
| `limit` / `starting_after` / `ending_before` | Paginación por cursor (default 10, máximo 100). |

### Ejemplo

**curl**

```bash
curl "https://api.vexorpay.com/api/v1/disputes?status=needs_response" \
  -H "Authorization: Bearer vxp_u_tu_clave"
```

**Node.js**

```js
const res = await fetch(
  'https://api.vexorpay.com/api/v1/disputes?status=needs_response',
  {
    headers: { Authorization: 'Bearer vxp_u_tu_clave' },
  },
)

const data = await res.json()
```

**PHP**

```php
<?php

$client = curl_init(
  'https://api.vexorpay.com/api/v1/disputes?status=needs_response',
);
curl_setopt_array($client, [
  CURLOPT_RETURNTRANSFER => true,
  CURLOPT_HTTPHEADER => ['Authorization: Bearer vxp_u_tu_clave'],
]);

$data = json_decode(curl_exec($client), true);
```

**Python**

```python

response = requests.get(
    'https://api.vexorpay.com/api/v1/disputes',
    params={'status': 'needs_response'},
    headers={'Authorization': 'Bearer vxp_u_tu_clave'},
)
data = response.json()
```

**Go**

```go
req, _ := http.NewRequest("GET", "https://api.vexorpay.com/api/v1/disputes?status=needs_response", nil)
req.Header.Set("Authorization", "Bearer vxp_u_tu_clave")

res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
```

### Respuesta exitosa

`200 OK`

```json
{
  "data": [
    {
      "id": "68f1c1e2c8a4b0d1f2e3a4b5",
      "chargeId": "67f1c1e2c8a4b0d1f2e3a4b5",
      "paymentLinkId": "66f1c1e2c8a4b0d1f2e3a4b5",
      "reference": "ORD-1001",
      "customerEmail": "cliente@mail.com",
      "stripeDisputeId": "dp_1UC0lPPRu4jOz4R",
      "status": "needs_response",
      "statusLabel": "Requiere respuesta",
      "reason": "fraudulent",
      "reasonLabel": "Pago fraudulento",
      "amountCents": 1900,
      "currency": "usd",
      "feeCents": 1500,
      "evidenceDeadline": "2026-10-15T12:00:00.000Z",
      "daysLeft": 7,
      "isOpen": true,
      "canRespond": true,
      "canAccept": true,
      "outcome": null,
      "recoveredCents": null,
      "compensatedCents": null,
      "sandbox": false,
      "paymentLinkUrl": "https://www.vexorpay.com/p/a1b2c3d4e5",
      "createdAt": "2026-10-08T12:03:00.000Z",
      "resolvedAt": null
    }
  ],
  "has_more": false,
  "total_count": 1,
  "url": "/v1/disputes?limit=10"
}
```

Campos clave:

- `status`: estado de la disputa. Abiertas: `needs_response`, `under_review` (y las consultas tempranas `warning_needs_response`, `warning_under_review`). Cerradas: `won`, `lost`, `expired`, `warning_closed`.
- `isOpen` / `canRespond`: `canRespond` es `false` cuando ya venció el plazo o la disputa está cerrada.
- `canAccept`: si se puede aceptar (perder) la disputa. Es `false` para las consultas tempranas (`warning_*`), que se cierran solas.
- `feeCents`: la fee de Stripe por la disputa (USD 15), que se cobra al creador en todos los desenlaces.
- `evidenceDeadline` / `daysLeft`: plazo para enviar evidencia (`null` cuando no aplica o ya venció).

## Obtener una disputa

`GET /api/v1/disputes/{id}`

El id es el id local de la disputa (el campo `id` de la lista, no el `stripeDisputeId`). En sandbox, mandá `?sandbox=true`.

Además del resumen, la respuesta incluye el estado **en vivo** de Stripe (por si el webhook todavía no llegó), la evidencia ya cargada, el costo estimado y el enlace al panel de Stripe.

```json
{
  "id": "68f1c1e2c8a4b0d1f2e3a4b5",
  "stripeDisputeId": "dp_1UC0lPPRu4jOz4R",
  "status": "needs_response",
  "stripe": {
    "status": "needs_response",
    "evidenceDueBy": "2026-10-15T12:00:00.000Z",
    "hasEvidence": false,
    "submissionCount": 0,
    "pastDue": false,
    "cardBrand": "visa",
    "cardCaseType": "fraudulent",
    "networkReasonCode": "10.4",
    "reason": "fraudulent"
  },
  "evidence": {
    "productDescription": null,
    "customerCommunication": null,
    "receipt": null,
    "uncategorizedText": null
  },
  "cost": {
    "amountInDisputeCents": 1900,
    "feeCents": 1500,
    "feeChargedToCreator": true,
    "recoveredCents": null,
    "compensatedCents": null
  },
  "paymentLinkUrl": "https://www.vexorpay.com/p/a1b2c3d4e5",
  "stripeDashboardUrl": "https://dashboard.stripe.com/disputes/dp_1UC0lPPRu4jOz4R"
}
```

Devuelve la disputa o `404 dispute_not_found`.

## Responder una disputa

`POST /api/v1/disputes/{id}/response`

Carga evidencia en una disputa abierta y la envía a Stripe (`submit: true`). La evidencia va en **snake_case**; al menos un campo debe tener contenido. Una vez enviada, la disputa pasa a `under_review` y el plazo deja de aplicar.

Soporta `Idempotency-Key`: un replay con la misma clave y el mismo body devuelve el detalle actual de la disputa sin re-enviar evidencia (`200`); con la misma clave y un body distinto responde `409 idempotency_conflict`.

### Body

| Campo | Descripción |
| --- | --- |
| `product_description` | Qué se entregó/servició y por qué el cobro es legítimo (campo principal). |
| `customer_communication` | Conversación con el cliente (se concatena con `notes` en `uncategorized_text`). |
| `notes` | Notas libres. |
| `service_date` | Fecha de entrega/servicio (`YYYY-MM-DD`). |
| `shipping_carrier` / `shipping_tracking_number` | Envío (para `product_not_received`). |
| `refund_policy_url` | URL pública de la política de reembolso. |
| `cancellation_policy_url` | URL pública de la política de cancelación. |
| `cancellation_rebuttal` | Refutación de la cancelación (suscripciones). |
| `duplicate_charge_id` / `duplicate_charge_explanation` | Cobro duplicado (reason `duplicate`). |
| `attach_receipt` | Adjunta el comprobante PDF del pago como evidencia (default `false`). |
| `sandbox` | `true` para operar sobre disputas de prueba. |

### Ejemplo

**curl**

```bash
curl https://api.vexorpay.com/api/v1/disputes/68f1c1e2c8a4b0d1f2e3a4b5/response \
  -X POST \
  -H "Authorization: Bearer vxp_u_tu_clave" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: dispute-68f1c1e2-response-1" \
  -d '{
    "product_description": "Ebook de UX en PDF enviado por email el 2026-01-02.",
    "customer_communication": "El cliente confirmó la recepción por email.",
    "notes": "Entrega verificada en el log de envíos.",
    "attach_receipt": true
  }'
```

**Node.js**

```js
const res = await fetch(
  'https://api.vexorpay.com/api/v1/disputes/68f1c1e2c8a4b0d1f2e3a4b5/response',
  {
    method: 'POST',
    headers: {
      Authorization: 'Bearer vxp_u_tu_clave',
      'Content-Type': 'application/json',
      'Idempotency-Key': 'dispute-68f1c1e2-response-1',
    },
    body: JSON.stringify({
      product_description: 'Ebook de UX en PDF enviado por email el 2026-01-02.',
      customer_communication: 'El cliente confirmó la recepción por email.',
      notes: 'Entrega verificada en el log de envíos.',
      attach_receipt: true,
    }),
  },
)

const dispute = await res.json()
```

**PHP**

```php
<?php

$client = curl_init(
  'https://api.vexorpay.com/api/v1/disputes/68f1c1e2c8a4b0d1f2e3a4b5/response',
);
curl_setopt_array($client, [
  CURLOPT_RETURNTRANSFER => true,
  CURLOPT_POST => true,
  CURLOPT_HTTPHEADER => [
    'Authorization: Bearer vxp_u_tu_clave',
    'Content-Type: application/json',
    'Idempotency-Key: dispute-68f1c1e2-response-1',
  ],
  CURLOPT_POSTFIELDS => json_encode([
    'product_description' => 'Ebook de UX en PDF enviado por email el 2026-01-02.',
    'customer_communication' => 'El cliente confirmó la recepción por email.',
    'notes' => 'Entrega verificada en el log de envíos.',
    'attach_receipt' => true,
  ]),
]);

$dispute = json_decode(curl_exec($client), true);
```

**Python**

```python

response = requests.post(
    'https://api.vexorpay.com/api/v1/disputes/68f1c1e2c8a4b0d1f2e3a4b5/response',
    headers={
        'Authorization': 'Bearer vxp_u_tu_clave',
        'Idempotency-Key': 'dispute-68f1c1e2-response-1',
    },
    json={
        'product_description': 'Ebook de UX en PDF enviado por email el 2026-01-02.',
        'customer_communication': 'El cliente confirmó la recepción por email.',
        'notes': 'Entrega verificada en el log de envíos.',
        'attach_receipt': True,
    },
)
dispute = response.json()
```

**Go**

```go
body, _ := json.Marshal(map[string]any{
  "product_description":     "Ebook de UX en PDF enviado por email el 2026-01-02.",
  "customer_communication": "El cliente confirmó la recepción por email.",
  "notes":                  "Entrega verificada en el log de envíos.",
  "attach_receipt":         true,
})

req, _ := http.NewRequest(
  "POST",
  "https://api.vexorpay.com/api/v1/disputes/68f1c1e2c8a4b0d1f2e3a4b5/response",
  bytes.NewReader(body),
)
req.Header.Set("Authorization", "Bearer vxp_u_tu_clave")
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Idempotency-Key", "dispute-68f1c1e2-response-1")

res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
```

### Respuesta exitosa

`200 OK` con el detalle de la disputa actualizado (mismo shape que `GET /api/v1/disputes/{id}`). El campo `stripe.status` pasa a `under_review`.

> **Sobre los textos de evidencia:** la conversación con el cliente (`customer_communication`) y tus `notes` se concatenan en `uncategorized_text` de Stripe. Cada campo de texto admite hasta 20.000 caracteres: los valores más largos se recortan para que la evidencia nunca falle por largo.

## Errores

| Error | HTTP | Causa |
| --- | --- | --- |
| `validation_error` | 400 | `status` no es un estado válido en el listado. |
| `dispute_not_found` | 404 | No existe esa disputa en tu cuenta (o es de otro modo). |
| `dispute_not_respondable` | 400 | La disputa ya está cerrada: no admite respuesta. |
| `dispute_deadline_passed` | 400 | Venció el plazo para enviar evidencia. |
| `evidence_required` | 400 | Ningún campo de evidencia tiene contenido. |
| `dispute_evidence_rejected` | 400 | Stripe rechazó la evidencia (el mensaje trae el detalle). |
| `idempotency_conflict` | 409 | La `Idempotency-Key` ya se usó con un body distinto. |
