# 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 sobre Polar (Merchant of Record), sin SDK oficial**. Migración: <https://developers.vexorpay.com/docs/migracion-desde-el-sdk>

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

## Base URL

```
https://api.vexorpay.com/api/v1
```

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

## Modo sandbox

El modo lo define la **API key**, no el request: las claves `vxp_live_...` operan
sobre producción y las `vxp_test_...` sobre datos de prueba. No existe un
parámetro `sandbox` en los requests. Cada host solo acepta claves de su modo y una
clave en el host equivocado responde `403 livemode_mismatch`. Ver
[Modo sandbox](/docs/sandbox-mode).

## Suscripción a vexorpay

Usar vexorpay en producción requiere la **suscripción a vexorpay** (USD 9 por mes con
**impuestos incluidos**, con **30 días de prueba gratis** al empezar). Si la suscripción de tu cuenta no está
en prueba ni al día, todas las operaciones de la API y del MCP responden
`402 subscription_required`. Empieza la prueba o
renueva la suscripción desde el dashboard. En el modo
sandbox no se exige.

## 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`), los reembolsos (`POST /api/v1/orders/{id}/refund`) y los retiros (`POST /api/v1/payouts`). Recomendado: usar una clave por operación (p. ej. el UUID del pedido) para no duplicar un reembolso o un retiro 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 `reason` en un `409 polar_not_ready`). 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_seats` | 400 | `seats` fuera de los límites del plan, o `seats` en un plan de precio fijo. |
| `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 precios 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_pricing_type` | 400 | `pricing_type` de un plan no es `flat` ni `per_seat`. |
| `invalid_seat_tiers` | 400 | La matriz `seat_tiers` de un plan es inválida. |
| `invalid_tiers_mode` | 400 | `tiers_mode` no es `graduated` ni `volume`, o viene sin `seat_tiers`. |
| `invalid_trial` | 400 | El `trial_period_days` está fuera de rango (1–90). |
| `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 de Polar. |
| `customer_not_found` | 404 | No existe ese cliente de Polar. |
| `price_not_found` | 404 | El `price_id` no es un plan de ese link (o de esa suscripción). |
| `not_found` | 404 | La `Idempotency-Key` apunta a un recurso que ya no existe. |
| `dispute_not_respondable` | 400 | La disputa ya está cerrada o no se puede aceptar. |
| `account_not_connected` | 400 | El creador no conectó Polar: no se pueden crear links de pago. |
| `payment_link_inactive` | 400 | El link de pago no está activo. |
| `polar_test_key_not_supported` | 400 | Una clave de prueba (`vxp_test_`) no puede crear links en producción. |
| `whop_link_failed` | 502 | El procesador de pagos no creó el link. Reintenta en unos minutos. |
| `whop_reconnect_required` | 409 | El procesador de pagos rechazó la operación por permisos de la cuenta de cobro: escríbenos desde soporte. |
| `idempotency_conflict` | 409 | La `Idempotency-Key` ya se usó con un body distinto. |
| `polar_not_ready` | 409 | La organización de Polar del creador todavía no puede cobrar (falta verificación en Polar). `detail.reason` explica el motivo. |
| `polar_link_not_editable_yet` | 409 | El link de Polar todavía se está creando (o su creación falló): reintenta en unos segundos. |
| `polar_reconnect_required` | 409 | La conexión con Polar es anterior al checkout por API: el creador tiene que volver a conectar Polar desde el dashboard. |
| `polar_link_structure_change` | 400 | En un link de Polar se cambian título, descripción, imagen, monto y nombre/monto/prueba de cada plan fijo. Agregar o quitar planes, cambiar intervalo o asientos exige un link nuevo. |
| `polar_field_not_supported` | 400 | El valor no aplica a un link de Polar (p. ej. `status: "canceled"`: en Polar un link está activo o archivado). |
| `polar_not_connected` | 409 | El creador no tiene Polar conectado. |
| `payout_account_not_ready` | 409 | La cuenta de cobro de Polar del creador no está lista: hay que terminar de configurarla en Polar. |
| `insufficient_balance` | 400 | El saldo disponible de Polar no alcanza para un retiro (Polar descuenta sus comisiones de retiro). |
| `payout_in_progress` | 409 | Ya hay un retiro de Polar creándose para esa cuenta: reintenta en unos minutos. |
| `polar_payout_failed` | 502 | Polar no procesó el retiro o no respondió. |
| `already_processed` | 409 | El recurso ya fue eliminado o archivado (p. ej. un link archivado). |
| `unsupported_plan` | 400 | Un plan recurrente no se puede expresar en Polar (p. ej. una combinación de precio por asiento no soportada). |
| `subscription_required` | 402 | La suscripción a vexorpay de tu cuenta no está en prueba ni al día (solo en producción). |
| `rate_limited` | 429 | Superaste el límite de 100 requests por minuto. |
| `polar_provisioning_failed` | 500 | Polar rechazó la creación del link. No quedó nada publicado: reintentá. |
| `polar_update_failed` | 502 | Polar rechazó o no respondió la edición (link o suscripción). No se cambió nada: reintenta. |
| `polar_archive_failed` | 502 | Polar no archivó el link. Sigue activo en los dos lados: reintenta. |
| `polar_checkout_failed` | 502 | Polar no creó el checkout. Reintenta en unos minutos. |
| `upstream_error` | 500 | La operación falló de forma inesperada en Polar u otro proveedor externo. Reintenta más tarde. |
| `unexpected_error` | 500 | Error interno inesperado. |

## 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}`, `PATCH /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` / `POST /api/v1/payouts`, `GET /api/v1/payouts/estimate`
- [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
