# Listar y obtener links

Consulta, actualiza y cancela los links de pago de tu cuenta con filtros y paginación.

> [!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/payment-links>

Los links eliminados nunca aparecen en los listados.

## Listar

`GET /api/v1/payment-links`

### Parámetros

| Parámetro | Descripción |
| --- | --- |
| `status` | Filtro: `active`, `canceled` o `archived`. Sin filtro se excluyen solo los eliminados. |
| `type` | Filtro: `one_time` o `recurring`. Sin filtro devuelve ambos. |
| `limit` | Cantidad por página (default 10, máximo 100). |
| `starting_after` / `ending_before` | Cursor de paginación por `id`. |

### Ejemplo

**curl**

```bash
curl "https://api.vexorpay.com/api/v1/payment-links?status=active&type=one_time&limit=10" \
  -H "Authorization: Bearer vxp_live_tu_clave"
```

**Node.js**

```js
const res = await fetch(
  'https://api.vexorpay.com/api/v1/payment-links?status=active&type=one_time&limit=10',
  {
    headers: { Authorization: 'Bearer vxp_live_tu_clave' },
  },
)

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

**PHP**

```php
<?php

$client = curl_init(
  'https://api.vexorpay.com/api/v1/payment-links?status=active&type=one_time&limit=10',
);
curl_setopt_array($client, [
  CURLOPT_RETURNTRANSFER => true,
  CURLOPT_HTTPHEADER => ['Authorization: Bearer vxp_live_tu_clave'],
]);

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

**Python**

```python

response = requests.get(
    'https://api.vexorpay.com/api/v1/payment-links',
    params={
        'status': 'active',
        'type': 'one_time',
        'limit': '10',
    },
    headers={'Authorization': 'Bearer vxp_live_tu_clave'},
)
data = response.json()
```

**Go**

```go
req, _ := http.NewRequest("GET", "https://api.vexorpay.com/api/v1/payment-links?status=active&type=one_time&limit=10", nil)
req.Header.Set("Authorization", "Bearer vxp_live_tu_clave")

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

### Respuesta exitosa

`200 OK`

```json
{
  "data": [
    {
      "id": "66f1c1e2c8a4b0d1f2e3a4b5",
      "amountCents": 1900,
      "currency": "usd",
      "title": "Ebook UX en 30 días",
      "description": null,
      "image": "https://.../product/66f1c1e2c8a4b0d1f2e3a4c9",
      "reference": "CATALOGO-UX",
      "metadata": null,
      "status": "active",
      "slug": "a1b2c3d4e5",
      "sandbox": false,
      "type": "one_time",
      "checkoutUrl": "https://buy.polar.sh/polar_cl_...",
      "url": "https://www.vexorpay.com/p/a1b2c3d4e5",
      "createdAt": "2026-01-02T12:00:00.000Z",
      "updatedAt": "2026-01-02T12:00:00.000Z"
    }
  ],
  "has_more": false,
  "total_count": 1,
  "url": "/v1/payment-links?limit=10"
}
```

En un link `recurring` el `amountCents` es `0` (el precio lo definen los planes) y se agrega el array `prices`:

```json
{
  "id": "66f1c1e2c8a4b0d1f2e3a4b7",
  "amountCents": 0,
  "currency": "usd",
  "type": "recurring",
  "prices": [
    {
      "id": "66f1c1e2c8a4b0d1f2e3a4c1",
      "amountCents": 900,
      "currency": "usd",
      "interval": "month",
      "intervalCount": 1,
      "trialPeriodDays": 7,
      "description": "Plan mensual",
      "isDefault": true
    }
  ]
}
```

## Obtener un link

`GET /api/v1/payment-links/{id}`

El entorno lo define la API key: una clave `vxp_test_...` opera sobre links de prueba.

**curl**

```bash
curl https://api.vexorpay.com/api/v1/payment-links/66f1c1e2c8a4b0d1f2e3a4b5 \
  -H "Authorization: Bearer vxp_live_tu_clave"
```

**Node.js**

```js
const res = await fetch(
  'https://api.vexorpay.com/api/v1/payment-links/66f1c1e2c8a4b0d1f2e3a4b5',
  {
    headers: { Authorization: 'Bearer vxp_live_tu_clave' },
  },
)

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

**PHP**

```php
<?php

$client = curl_init(
  'https://api.vexorpay.com/api/v1/payment-links/66f1c1e2c8a4b0d1f2e3a4b5',
);
curl_setopt_array($client, [
  CURLOPT_RETURNTRANSFER => true,
  CURLOPT_HTTPHEADER => ['Authorization: Bearer vxp_live_tu_clave'],
]);

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

**Python**

```python

response = requests.get(
    'https://api.vexorpay.com/api/v1/payment-links/66f1c1e2c8a4b0d1f2e3a4b5',
    headers={'Authorization': 'Bearer vxp_live_tu_clave'},
)
data = response.json()
```

**Go**

```go
req, _ := http.NewRequest("GET", "https://api.vexorpay.com/api/v1/payment-links/66f1c1e2c8a4b0d1f2e3a4b5", nil)
req.Header.Set("Authorization", "Bearer vxp_live_tu_clave")

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

Devuelve el link completo de tu cuenta o `404 link_not_found` si no existe. `checkoutUrl` es el checkout de Polar del link.

`url` es el link público para compartir: cada visita genera una referencia única para esa compra, que vuelve en `reference` de la venta, sus webhooks y `/events`. Si quieres usar tu propio id, agrega `?reference=<tu-id>` (por ejemplo `https://www.vexorpay.com/p/a1b2c3d4e5?reference=ORD-1001`). Ver [Identificar cada venta](/docs/api/orders#identificar-cada-venta-con-tu-referencia). Comparte `url`, no `checkoutUrl`: la referencia se reenvía a Polar desde `url`.

## Métricas de un link

`GET /api/v1/payment-links/{id}/stats`

Las mismas métricas que ves en el detalle del link en el dashboard. Los montos van en **centavos de USD** y son **netos**: sin impuestos, sin la comisión de Polar y sin lo reembolsado. Se calculan sobre las ventas de Polar del link.

Link de pago único (`type: "one_time"`):

```json
{
  "paymentLinkId": "68f1c1e2c8a4b0d1f2e3a4b5",
  "type": "one_time",
  "salesCount": 12,
  "grossCents": 66000,
  "netCents": 52740,
  "avgTicketCents": 4395,
  "customers": 11,
  "repeatCustomers": 1,
  "refunds": { "count": 1, "cents": 5000 },
  "disputes": { "count": 0, "cents": 0 },
  "topCountries": [{ "country": "US", "count": 5 }, { "country": "ES", "count": 3 }],
  "weekly": [{ "weekStart": "2026-10-20T00:00:00.000Z", "netCents": 8790, "count": 2 }]
}
```

Link recurrente (`type: "recurring"`):

```json
{
  "paymentLinkId": "68f1c1e2c8a4b0d1f2e3a4b6",
  "type": "recurring",
  "activeSubscribers": 18,
  "mrrCents": 36000,
  "trialing": { "count": 3, "mrrCents": 6000 },
  "atRisk": { "count": 1, "mrrCents": 2000 },
  "newThisMonth": 4,
  "canceledThisMonth": 1,
  "churnRate": 0.056,
  "trialConversion": { "converted": 7, "total": 9 },
  "netThisMonthCents": 31200,
  "netTotalCents": 214500,
  "ltvCents": 9750,
  "monthly": [{ "month": "2026-10", "netCents": 31200 }]
}
```

- `salesCount`, `customers` y `avgTicketCents` cuentan las ventas vigentes: las reembolsadas por completo van en `refunds`. `netCents` ya descuenta todos los reembolsos.
- `mrrCents` cuenta sólo las suscripciones `active`: las que están en prueba van en `trialing` (MRR potencial) y las de pago fallido en `atRisk`.
- `churnRate` = bajas del mes / suscriptores al empezar el mes (`null` sin base).
- `weekly` trae las últimas 12 semanas y `monthly` los últimos 12 meses.

Si el link no existe (o es de otro modo, prueba/producción) responde `404 link_not_found`.

## Actualizar un link

`PATCH /api/v1/payment-links/{id}`

El cambio se aplica en Polar. Puedes cambiar `title`, `description`, `image_file_id` (`null` quita la foto), `reference`, `amount` (en dólares, sólo pago único) y, en cada plan de un link `recurring`, `amount` (`0` = plan gratis), `description` y `trial_period_days`. El precio nuevo aplica a las compras nuevas: las suscripciones vigentes siguen con su precio. El `checkoutUrl` no cambia.

- Cada plan va con su `id` y la lista de planes tiene que ser la misma. Agregar o quitar planes, cambiar `interval`/`interval_count` o editar un plan por asiento responde `400 polar_link_structure_change`: crea un link nuevo. Sin `prices`, los planes quedan como están.
- El `type` no se puede editar (`400 validation_error`). El `amount` no aplica a links recurrentes ni `prices` a links de pago único (`validation_error`).
- `status` acepta `active` o `archived`: `archived` lo saca de venta en Polar, igual que [`DELETE`](#eliminar-un-link). Otro valor (p. ej. `canceled`) responde `400 polar_field_not_supported`. Un link archivado no se edita ni se reactiva (`400 payment_link_inactive`).

```bash
curl https://api.vexorpay.com/api/v1/payment-links/66f1c1e2c8a4b0d1f2e3a4b5 \
  -X PATCH \
  -H "Authorization: Bearer vxp_live_tu_clave" \
  -H "Content-Type: application/json" \
  -d '{
    "prices": [
      { "id": "66f1c1e2c8a4b0d1f2e3a4c1", "amount": 12, "interval": "month", "description": "Mensual" },
      { "id": "66f1c1e2c8a4b0d1f2e3a4c2", "amount": 120, "interval": "year", "trial_period_days": 7, "description": "Anual" }
    ]
  }'
```

Items de `prices`:

| Campo | Tipo | Necesario | Notas |
| --- | --- | --- | --- |
| `id` | string | sí | Id del plan existente (debe pertenecer a este link). |
| `amount` | number | sí | Monto por ciclo en dólares (mínimo USD 4, o `0` para un plan gratis de precio fijo). `amount * 100` es el `amountCents`. |
| `currency` | string | no | Solo `usd` (default). |
| `interval` | string | sí | `day`, `week`, `month` o `year`. |
| `interval_count` | number | no | Cada N intervalos (default `1`). |
| `trial_period_days` | number | no | Trial gratuito en días (`1` a `90`); ausente o `null` = sin trial. |
| `description` | string | no | Label del plan ("Mensual", "Anual"...). |
| `pricing_type` | string | no | `flat` (default) o `per_seat`. |
| `per_seat_amount` | number | no | Precio por asiento en dólares (variante "base + extra"). |
| `min_seats` | integer | no | Asientos mínimos (default 1, máximo 1000). |
| `max_seats` | integer | no | Asientos máximos. |
| `seat_tiers` | array \| null | no | Matriz de tramos (`up_to`, `unit_amount` o `flat_amount`, en centavos); `null` la borra. |
| `tiers_mode` | string | no | `graduated` o `volume`. Requiere `seat_tiers`. |

Los campos de asientos son los mismos que en
[crear un link recurrente](/docs/api/create-recurring-payment-link#planes-por-asientos), pero en un link existente no se pueden cambiar (es un cambio de estructura).

Devuelve `200` con el link actualizado y sus `prices`.

El entorno de `GET`, `PATCH` y `DELETE` lo define la API key, no un parámetro del request.

## Errores

| Error | HTTP | Causa |
| --- | --- | --- |
| `validation_error` | 400 | Intentaste cambiar `type`, mandaste `prices` en un link `one_time`, `amount` en un link `recurring`, `id` de un plan de otro link, un plan repetido, `currency` que no es USD o `interval` inválido. |
| `invalid_amount` | 400 | El nuevo `amount` es menor a USD 4. |
| `minimum_amount` | 400 | Algún plan con `amount` menor a USD 4. |
| `invalid_pricing_type` | 400 | `pricing_type` no es `flat` ni `per_seat`. |
| `invalid_seat_tiers` | 400 | Matriz `seat_tiers` inválida (no es lista, vacía, tramos pisos o de un plan de precio fijo). |
| `invalid_tiers_mode` | 400 | `tiers_mode` no es `graduated` ni `volume`, o se indicó sin `seat_tiers`. |
| `invalid_interval_count` | 400 | `interval_count` inválido (entero 1–12; `year` solo cada 1). |
| `invalid_trial` | 400 | `trial_period_days` fuera de 1–90. |
| `prices_required` | 400 | `prices` vacío o no es un array. |
| `too_many_prices` | 400 | Más de 5 planes. |
| `payment_link_inactive` | 400 | El link está archivado: no se edita ni se reactiva. |
| `invalid_request` | 400 | El body del PATCH está vacío. |
| `polar_link_structure_change` | 400 | Cambio de estructura (planes, intervalo o asientos). Crea un link nuevo. |
| `polar_field_not_supported` | 400 | `status` distinto de `active` o `archived` (p. ej. `canceled`, que no existe en Polar). |
| `polar_link_not_editable_yet` | 409 | El link de Polar todavía se está creando: reintenta en unos segundos. |
| `polar_update_failed` | 502 | Polar rechazó o no respondió el cambio. No se cambió nada. |
| `link_not_found` | 404 | No existe un link con ese id en tu cuenta (o es de otro modo). |

## Eliminar un link

`DELETE /api/v1/payment-links/{id}`

Alineado con la acción **Eliminar** del dashboard: saca el link de venta y nunca
borra ventas ya realizadas.

```bash
curl -X DELETE https://api.vexorpay.com/api/v1/payment-links/66f1c1e2c8a4b0d1f2e3a4b5 \
  -H "Authorization: Bearer vxp_live_tu_clave"
```

| Situación | HTTP | Respuesta |
| --- | --- | --- |
| Link activo | `200` | El link con `status: "archived"`: sus productos se archivan en Polar y su `checkoutUrl` deja de funcionar. Las suscripciones vigentes siguen cobrándose en Polar hasta que se cancelen. |
| Link ya `deleted` o `archived` | `409` | `already_processed`. |
| No existe, es de otro usuario o de otro modo | `404` | `link_not_found`. |

Para que dejen de cobrarse las suscripciones de un link archivado, cancela cada una con
[`POST /api/v1/subscriptions/{id}/cancel`](/docs/api/subscriptions).

```json
{
  "id": "66f1c1e2c8a4b0d1f2e3a4b5",
  "amountCents": 499,
  "currency": "usd",
  "title": "Pack de presets",
  "description": null,
  "image": null,
  "reference": null,
  "metadata": {},
  "status": "archived",
  "slug": "a1b2c3d4e5",
  "sandbox": false,
  "url": "https://www.vexorpay.com/p/a1b2c3d4e5",
  "type": "one_time",
  "createdAt": "2026-01-02T12:00:00.000Z",
  "updatedAt": "2026-02-02T09:30:00.000Z"
}
```

Errores:

| Error | HTTP | Causa |
| --- | --- | --- |
| `already_processed` | 409 | El link ya fue eliminado o archivado. |
| `polar_archive_failed` | 502 | Polar no archivó el link. Sigue activo en los dos lados: reintenta. |
| `link_not_found` | 404 | No existe un link con ese id en tu cuenta (o es de otro modo). |
