# Crear link de pago recurrente

Crea planes por suscripción (pagos recurrentes) en dólares con la API de vexorpay.

> [!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/create-recurring-payment-link>

Un link recurrente agrupa hasta **5 planes** (precios por ciclo) y cobra automáticamente cada período. El comprador elige un plan en `url` y paga en el checkout; a partir de ahí la suscripción se renueva sola cada ciclo, en USD, y cada renovación se acredita en tu saldo como una venta nueva.

Necesitas la cuenta de cobro activada (si no, `409 payment_account_not_ready`). Ver [Suscripciones](/docs/api/subscriptions).

## Request

`POST /api/v1/payment-links`

### Headers

| Header | Valor |
| --- | --- |
| `Authorization` | `Bearer vxp_live_tu_clave` |
| `Content-Type` | `application/json` |
| `Idempotency-Key` | opcional — para repetir sin duplicar |
| `Vexor-Account` | opcional — id del usuario conectado, para crear el link en su nombre ([cuentas conectadas](/docs/api/connected-accounts)) |

### Body

| Campo | Tipo | Requerido | Descripción |
| --- | --- | --- | --- |
| `type` | string | **sí** | `"recurring"`. |
| `prices` | array | **sí** | Entre 1 y 5 planes. |
| `title` | string | **sí** | Título del producto: lo ve el comprador en el checkout y en el recibo. |
| `description` | string | no | Descripción. |
| `image_file_id` | string | no | Foto del producto (archivo tuyo subido desde el dashboard). |
| `reference` | string | no | Referencia tuya para reconciliar. |
| `metadata` | object | no | Pares clave/valor libres. |

#### Cada elemento de `prices`

| Campo | Tipo | Requerido | Descripción |
| --- | --- | --- | --- |
| `amount` | number | **sí** | Precio por ciclo en dólares. Mínimo USD 4. No hay planes gratis. |
| `interval` | string | **sí** | `day`, `week`, `month` o `year`. |
| `interval_count` | number | no | Cada N intervalos (entero 1–12, default 1). El intervalo `year` solo admite 1. |
| `trial_period_days` | number | no | Días de prueba gratis (1–90). El primer cobro se hace al terminar la prueba. |
| `description` | string | no | Nombre visible del plan ("Básico", "Pro", "Anual"…). |

> Todos los planes deben ser en USD. El link en sí queda con `amountCents: 0`; el precio real vive en cada plan.

#### Planes gratis, asientos y pruebas

- **Planes gratis**: cada plan vale al menos USD 4. Un plan con `amount: 0` responde
  `400 unsupported_plan` y no se crea nada.
- **Precio por asiento**: una suscripción es siempre de una unidad. `pricing_type: "per_seat"` (o
  cualquier campo de asientos) responde `400 unsupported_plan`. Para equipos, crea
  un plan por tamaño (por ejemplo "Equipo 5" y "Equipo 10").
- **Prueba gratis**: al suscribirse con prueba, el comprador deja su tarjeta y se registra una
  venta de USD 0 (`billingReason: "subscription_create"`); el primer cobro real llega al terminar
  la prueba.

> La API rechaza con `validation_error` cualquier key desconocida dentro de un
> plan, así que un typo (por ejemplo `trial_days` en vez de `trial_period_days`) no se ignora en
> silencio: falla el request.

### Ejemplo

**curl**

```bash
curl https://api.vexorpay.com/api/v1/payment-links \
  -X POST \
  -H "Authorization: Bearer vxp_live_tu_clave" \
  -H "Content-Type: application/json" \
  -d '{
    "type": "recurring",
    "title": "Membresía Vexor",
    "reference": "MEMBRESIA",
    "prices": [
      {
        "amount": 9,
        "interval": "month",
        "description": "Mensual"
      },
      {
        "amount": 90,
        "interval": "year",
        "trial_period_days": 7,
        "description": "Anual"
      }
    ]
  }'
```

**Node.js**

```js
const res = await fetch('https://api.vexorpay.com/api/v1/payment-links', {
  method: 'POST',
  headers: {
    Authorization: 'Bearer vxp_live_tu_clave',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    type: 'recurring',
    title: 'Membresía Vexor',
    reference: 'MEMBRESIA',
    prices: [
      { amount: 9, interval: 'month', description: 'Mensual' },
      { amount: 90, interval: 'year', trial_period_days: 7, description: 'Anual' },
    ],
  }),
})

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

**PHP**

```php
<?php

$client = curl_init('https://api.vexorpay.com/api/v1/payment-links');
curl_setopt_array($client, [
  CURLOPT_POST => true,
  CURLOPT_RETURNTRANSFER => true,
  CURLOPT_HTTPHEADER => [
    'Authorization: Bearer vxp_live_tu_clave',
    'Content-Type: application/json',
  ],
  CURLOPT_POSTFIELDS => json_encode([
    'type' => 'recurring',
    'title' => 'Membresía Vexor',
    'reference' => 'MEMBRESIA',
    'prices' => [
      ['amount' => 9, 'interval' => 'month', 'description' => 'Mensual'],
      ['amount' => 90, 'interval' => 'year', 'trial_period_days' => 7, 'description' => 'Anual'],
    ],
  ]),
]);

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

**Python**

```python

response = requests.post(
    'https://api.vexorpay.com/api/v1/payment-links',
    headers={
        'Authorization': 'Bearer vxp_live_tu_clave',
        'Content-Type': 'application/json',
    },
    json={
        'type': 'recurring',
        'title': 'Membresía Vexor',
        'reference': 'MEMBRESIA',
        'prices': [
            {'amount': 9, 'interval': 'month', 'description': 'Mensual'},
            {
                'amount': 90,
                'interval': 'year',
                'trial_period_days': 7,
                'description': 'Anual',
            },
        ],
    },
)
data = response.json()
```

**Go**

```go
body, _ := json.Marshal(map[string]any{
    "type":      "recurring",
    "title":     "Membresía Vexor",
    "reference": "MEMBRESIA",
    "prices": []map[string]any{
        {"amount": 9, "interval": "month", "description": "Mensual"},
        {"amount": 90, "interval": "year", "trial_period_days": 7, "description": "Anual"},
    },
})

req, _ := http.NewRequest("POST", "https://api.vexorpay.com/api/v1/payment-links", bytes.NewReader(body))
req.Header.Set("Authorization", "Bearer vxp_live_tu_clave")
req.Header.Set("Content-Type", "application/json")

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

### Respuesta exitosa

`201 Created`

```json
{
  "id": "66f1c1e2c8a4b0d1f2e3a4b6",
  "amountCents": 0,
  "currency": "usd",
  "title": "Membresía Vexor",
  "description": null,
  "image": null,
  "reference": "MEMBRESIA",
  "metadata": null,
  "status": "active",
  "slug": "b2c3d4e5f6",
  "sandbox": false,
  "url": "https://www.vexorpay.com/p/b2c3d4e5f6",
  "checkoutUrl": "https://…",
  "type": "recurring",
  "prices": [
    {
      "id": "66f1c1e2c8a4b0d1f2e3a4c1",
      "amountCents": 900,
      "currency": "usd",
      "interval": "month",
      "intervalCount": 1,
      "trialPeriodDays": null,
      "description": "Mensual",
      "isDefault": true
    },
    {
      "id": "66f1c1e2c8a4b0d1f2e3a4c2",
      "amountCents": 9000,
      "currency": "usd",
      "interval": "year",
      "intervalCount": 1,
      "trialPeriodDays": 7,
      "description": "Anual",
      "isDefault": false
    }
  ],
  "createdAt": "2026-01-02T12:00:00.000Z",
  "updatedAt": "2026-01-02T12:00:00.000Z"
}
```

El comprador elige el plan desde `url` (la página pública del link), o puedes llevarlo directo a un plan con [`POST /v1/payment-links/{id}/checkout`](/docs/api/checkout) y `price_id`. El **tipo** del link no se puede cambiar con `PATCH`, y de los planes existentes solo se edita el nombre (`description`): cambiar el precio, el intervalo, la prueba gratis o la cantidad de planes exige un link nuevo (ver [Listar y gestionar links](/docs/api/payment-links)).

## Errores

| Error | HTTP | Causa |
| --- | --- | --- |
| `unauthorized` | 401 | Falta el header `Authorization`. |
| `invalid_key` | 401 | La API key es inválida o fue revocada. |
| `livemode_mismatch` | 403 | La clave no corresponde al host (clave de prueba contra producción o al revés). |
| `prices_required` | 400 | `type: "recurring"` sin `prices[]`. |
| `too_many_prices` | 400 | Más de 5 planes. |
| `minimum_amount` | 400 | Algún plan es menor a USD 4. |
| `invalid_interval` | 400 | `interval` distinto de `day`, `week`, `month` o `year`. |
| `invalid_interval_count` | 400 | `interval_count` fuera de 1–12 (o `year` ≠ 1). |
| `invalid_trial` | 400 | `trial_period_days` fuera de 1–90. |
| `unsupported_plan` | 400 | Plan gratis (`amount: 0`) o precio por asiento: no se pueden cobrar. |
| `validation_error` | 400 | Falta el `title` (o está vacío), u otros datos inválidos (p. ej. moneda distinta de USD o una key desconocida dentro de un plan). |
| `payment_account_not_ready` | 409 | Tu cuenta de cobro no está activada (o fue rechazada): actívala desde el dashboard. |
| `idempotency_conflict` | 409 | La `Idempotency-Key` ya se usó con un body distinto. |
| `rate_limited` | 429 | Superaste el límite de 100 requests por minuto. |
| `payment_link_failed` | 502 | El procesador de pagos rechazó la creación del link. No quedó nada publicado: reintenta. |
