# Referencia de API

Referencia general de la API de vexorpay: base URL, autenticación, formato, paginación, idempotencia, rate limit y errores.

> [!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 de links de pago en USD, sin SDK oficial**. Migración: <https://developers.vexorpay.com/docs/migracion-desde-el-sdk>

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

## Base URL

```
https://api.vexorpay.com/api/v1           # producción (claves vxp_live_)
https://api.sandbox.vexorpay.com/api/v1   # sandbox (claves vxp_test_)
```

## Autenticación

Todos los endpoints requieren tu clave de API, enviada en el header `Authorization`:

```
Authorization: Bearer vxp_live_tu_clave
```

Aprende más en [Autenticación](/docs/api/authentication).

> **Conexiones OAuth:** las apps conectadas pueden autenticarse con un token
> OAuth (`vxo_at_…`) en vez de una API key. En ese caso la conexión fija el modo
> (sandbox o producción) y cada endpoint exige un **scope**: si la conexión no lo
> tiene, la API responde `403 insufficient_scope`. Ver [vexorpay MCP](/docs/mcp).

## Formato de respuesta

- Los **recursos** (links, ventas, suscripciones) se devuelven como **objeto plano** (p. ej. `GET /api/v1/payment-links/{id}` o `GET /api/v1/orders/{id}`).
- Los **listados** se devuelven en un envoltorio con paginación:

```json
{
  "data": [ { "...": "recurso" } ],
  "has_more": false,
  "total_count": 12,
  "url": "/v1/orders?limit=10"
}
```

## Paginación

Los listados usan paginación por cursor:

| Parámetro | Descripción |
| --- | --- |
| `limit` | Cantidad por página (default 10, mínimo 1, máximo 100). |
| `starting_after` | ID del último elemento de la página anterior; devuelve los siguientes. |
| `ending_before` | ID del primer elemento de la página anterior; devuelve los anteriores. |

`limit` tiene que ser un entero entre 1 y 100: con `0`, `-5`, `101`, `2.5` o un
valor no numérico la API responde `400 validation_error`. En la respuesta, la
`url` de paginación refleja el `limit` efectivo de esa página.

## Producción y sandbox

Hay dos entornos, cada uno con su host y su familia de claves. El modo lo define la
**API key**, no el request:

| Entorno | Base URL | Claves |
| --- | --- | --- |
| Producción | `https://api.vexorpay.com/api/v1` | `vxp_live_...` |
| Sandbox (pruebas) | `https://api.sandbox.vexorpay.com/api/v1` | `vxp_test_...` |

Sandbox es un entorno aparte, con sus propios usuarios y datos: las claves de prueba se
crean desde `dashboard.sandbox.vexorpay.com`. No existe un parámetro `sandbox` en los
requests. Una clave en el host equivocado responde `403 livemode_mismatch`. Ver
[Modo sandbox](/docs/sandbox-mode).

## Cobros y comisiones

- Los precios son **siempre en USD**, con un mínimo de **USD 4** por cobro y por plan. No hay
  planes gratis ni precios por asiento.
- Sin suscripción: cada venta paga una sola comisión de vexorpay, desde **2,7 % + USD 1**
  con el procesamiento incluido. Cada [venta](/docs/api/orders) trae el desglose en
  `feeCents` y `netCents`.
- En los pagos únicos el comprador puede pagar en su moneda local; las suscripciones se cobran
  en USD. Las ventas se informan siempre en USD.
- Para cobrar hace falta la **cuenta de cobro** activada desde el dashboard (si no,
  `409 payment_account_not_ready`). Ver [Cómo funcionan los cobros](/docs/como-funcionan-los-cobros).

## Idempotencia

Los `POST` que crean recursos aceptan el header `Idempotency-Key`. Si repites la misma clave con el **mismo body**, no se crea nada nuevo: se devuelve el recurso original (replay `200`). Con la misma clave y un **body distinto**, recibes un error `idempotency_conflict` (`409`).

Lo aceptan la creación de links (`POST /api/v1/payment-links`) y los reembolsos (`POST /api/v1/orders/{id}/refund`). Recomendado: usar una clave por operación (p. ej. el UUID del pedido) para no duplicar un reembolso si el request se repite. El checkout (`POST /api/v1/payment-links/{id}/checkout`) no es idempotente: cada llamada crea una sesión de checkout nueva.

Cada clave se conserva **24 horas**: pasado ese plazo se descarta y una clave repetida vuelve a ejecutar la operación (se crea un recurso nuevo).
Si el recurso original ya no existe (p. ej. fue eliminado dentro de esas 24 horas), el replay responde `404 not_found`.

## Rate limit

La API tiene un límite de **100 requests por minuto** por clave. Cada respuesta incluye los headers `X-RateLimit-Limit`, `X-RateLimit-Remaining` y `X-RateLimit-Reset` (también en los errores `401` y `429`). Al superarlo recibes `429 Too Many Requests` con `Retry-After` y `X-RateLimit-Remaining: 0`. Todas las respuestas traen además `X-Request-Id` (id único para cruzar con soporte) y `Cache-Control: no-store`.

## Errores

Los errores usan el formato:

```json
{
  "error": "invalid_amount",
  "message": "The minimum amount per charge is USD 4.",
  "detail": { "resetInSeconds": 42 }
}
```

`detail` es opcional y siempre un objeto (p. ej. `resetInSeconds` en un `429` o `required_scope` en un `403 insufficient_scope`). Nunca contiene mensajes crudos de errores internos: esos se registran del lado del servidor.

**Compatibilidad**: los campos desconocidos que mandes en un body se ignoran (no validan) y no se devuelven en la respuesta. La única excepción son las keys desconocidas **dentro de un plan** de un link recurrente (`prices[]`): ahí responden `400 validation_error` para que un typo no pase en silencio. Del otro lado, la API puede agregar campos nuevos a las respuestas en cualquier momento: trata los campos que no conozcas como opcionales.

| `error` | HTTP | Causa |
| --- | --- | --- |
| `unauthorized` | 401 | Falta el header `Authorization`. |
| `invalid_key` | 401 | La API key es inválida o fue revocada. |
| `insufficient_scope` | 403 | La conexión (token de una app conectada) no tiene el permiso que exige este endpoint. `detail.required_scope` indica cuál hace falta. |
| `livemode_mismatch` | 403 | La clave y el host no coinciden en modo: diste una clave de prueba contra `api.vexorpay.com` (o una live contra `api.sandbox.vexorpay.com`). |
| `invalid_connected_account` | 403 | El header `Vexor-Account` apunta a una cuenta sin conexión activa de la app, o la credencial no es de una app de marketplace. |
| `validation_error` | 400 | Algún parámetro tiene un valor inválido. |
| `invalid_request` | 400 | El body es inválido o falta un dato requerido. |
| `invalid_amount` | 400 | El monto no es válido o es menor a USD 4. |
| `minimum_amount` | 400 | El precio de un plan recurrente es menor a USD 4. |
| `prices_required` | 400 | Un link `recurring` necesita al menos un precio. |
| `too_many_prices` | 400 | Se superó el máximo de 5 planes por link. |
| `invalid_interval` | 400 | El `interval` del precio no es `day`, `week`, `month` o `year`. |
| `invalid_interval_count` | 400 | El `interval_count` está fuera de rango. |
| `invalid_trial` | 400 | El `trial_period_days` está fuera de rango (1–90). |
| `unsupported_plan` | 400 | El plan no se puede cobrar: plan gratis (USD 0), precio por asiento, o un cambio de planes en un link recurrente existente (precio, intervalo, prueba o cantidad de planes: hace falta un link nuevo). |
| `payment_link_inactive` | 400 | El link de pago no está activo. |
| `dispute_not_respondable` | 400 | La disputa ya está cerrada o no se puede aceptar. |
| `link_not_found` | 404 | No existe ese link de pago. |
| `subscription_not_found` | 404 | No existe esa suscripción. |
| `dispute_not_found` | 404 | No existe esa disputa. |
| `order_not_found` | 404 | No existe esa venta. |
| `customer_not_found` | 404 | No existe ese cliente. |
| `price_not_found` | 404 | El `price_id` no es un plan de ese link. |
| `not_found` | 404 | El recurso no existe (p. ej. una conexión de marketplace), o la `Idempotency-Key` apunta a un recurso que ya no existe. |
| `payment_account_not_ready` | 409 | La cuenta de cobro no está lista: todavía no se activó, fue rechazada o (para retirar) falta verificar la identidad. Se resuelve desde el dashboard. |
| `idempotency_conflict` | 409 | La `Idempotency-Key` ya se usó con un body distinto. |
| `already_processed` | 409 | El recurso ya fue eliminado o archivado (p. ej. un link archivado). |
| `rate_limited` | 429 | Superaste el límite de 100 requests por minuto. |
| `unexpected_error` | 500 | Error interno inesperado. |
| `payment_link_failed` | 502 | El procesador de pagos rechazó o no respondió la creación o la edición del link. No se cambió nada: reintenta en unos minutos. |
| `upstream_error` | 502 | El procesador de pagos falló o no respondió (checkout, reembolso, cancelación, disputa, saldo o retiro). No se cambió nada: reintenta más tarde. |

## Endpoints

- [Crear link de pago único](/docs/api/create-payment-link) — `POST /api/v1/payment-links`
- [Crear link de pago recurrente](/docs/api/create-recurring-payment-link) — `POST /api/v1/payment-links` con `type: "recurring"`
- [Listar y gestionar links](/docs/api/payment-links) — `GET /api/v1/payment-links`, `GET` / `PATCH` / `DELETE /api/v1/payment-links/{id}`, `GET /api/v1/payment-links/{id}/stats`
- [Checkout por compra](/docs/api/checkout) — `POST /api/v1/payment-links/{id}/checkout`
- [Ventas](/docs/api/orders) — `GET /api/v1/orders`, `GET /api/v1/orders/{id}`, `POST /api/v1/orders/{id}/refund`
- [Clientes](/docs/api/customers) — `GET /api/v1/customers`, `GET /api/v1/customers/{id}`
- [Disputas](/docs/api/disputes) — `GET /api/v1/disputes`, `GET /api/v1/disputes/{id}`, `POST /api/v1/disputes/{id}/accept`
- [Suscripciones](/docs/api/subscriptions) — `GET /api/v1/subscriptions`, `GET /api/v1/subscriptions/{id}`, `POST /api/v1/subscriptions/{id}/cancel`, `POST /api/v1/subscriptions/{id}/portal`
- [Saldo](/docs/api/balance) — `GET /api/v1/balance`
- [Retiros](/docs/api/payouts) — `GET /api/v1/payouts` (sólo lectura: los retiros se piden en el portal de retiros)
- [Eventos de reconciliación](/docs/api/events) — `GET /api/v1/events`
- [Cuentas conectadas (marketplace)](/docs/api/connected-accounts) — `Vexor-Account`, `GET /api/v1/connected-accounts`, `GET` / `DELETE /api/v1/connected-accounts/{id}`
- [Webhooks salientes](/docs/api/webhooks) — configurar y verificar notificaciones
- [Autenticación](/docs/api/authentication) — claves, rotación y seguridad
- [Modo sandbox](/docs/sandbox-mode) — prueba tu integración sin mover dinero real
