# Checkout por compra

Genera una sesión de checkout de Polar por cada pedido sobre un link existente.

> [!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/checkout>

Genera una **sesión de checkout de Polar** sobre un link existente y te devuelve un `checkoutUrl` listo para redirigir al cliente.

Es el caso de uso "link reutilizable que se paga N veces": cada llamada crea una sesión nueva, con el email del comprador precargado y tu referencia del pedido. Polar es el Merchant of Record: cobra, calcula el impuesto y le manda el comprobante al comprador. La venta aparece como [orden](/docs/api/orders) cuando el comprador paga, y se dispara el webhook `payment.completed`.

> En un link **`one_time`** el checkout cobra el monto del link. En un link **`recurring`** lleva directo al plan indicado con `price_id` (o al default), sin pasar por el selector de planes de la página pública `url` (`/p/{slug}`). Ver [Suscripciones](/docs/api/subscriptions).

## Request

`POST /api/v1/payment-links/{id}/checkout`

### Headers

| Header | Valor |
| --- | --- |
| `Authorization` | `Bearer vxp_live_tu_clave` |
| `Content-Type` | `application/json` |
| `Vexor-Account` | opcional — id del usuario conectado, para cobrar un link suyo ([cuentas conectadas](/docs/api/connected-accounts)) |

`Idempotency-Key` no aplica: cada llamada crea una sesión de checkout nueva.

### Body

| Campo | Tipo | Requerido | Descripción |
| --- | --- | --- | --- |
| `reference` | string | no | Tu referencia del pedido (p. ej. `ORD-1001`, hasta 200 caracteres). Si se omite, se genera una única (`VXP-…`). Vuelve en la respuesta, en la venta (`reference` de [`GET /orders`](/docs/api/orders)), en sus webhooks y en `/events`. |
| `customer_email` | string | no | Email del comprador (precarga el checkout). |
| `metadata` | object | no | Pares clave/valor del pedido. Sólo valores planos: texto, número o booleano. Vuelven en la `metadata` de la venta. |
| `price_id` | string | no | Solo para links `recurring`: id del plan (`prices[].id`) al que se suscribe el comprador. Ausente = plan default (`isDefault` o el primero). En un link `one_time` responde `400 validation_error`. |
| `seats` | integer | no | Solo para planes `per_seat`: cuántos asientos compra el comprador. Si se omite, se compran los `minSeats` del plan. |

### Ejemplo

**curl**

```bash
curl https://api.vexorpay.com/api/v1/payment-links/66f1c1e2c8a4b0d1f2e3a4b5/checkout \
  -X POST \
  -H "Authorization: Bearer vxp_live_tu_clave" \
  -H "Content-Type: application/json" \
  -d '{
    "reference": "ORD-1001",
    "customer_email": "cliente@mail.com",
    "metadata": { "userId": "u_42" }
  }'
```

**Node.js**

```js
const res = await fetch(
  'https://api.vexorpay.com/api/v1/payment-links/66f1c1e2c8a4b0d1f2e3a4b5/checkout',
  {
    method: 'POST',
    headers: {
      Authorization: 'Bearer vxp_live_tu_clave',
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      reference: 'ORD-1001',
      customer_email: 'cliente@mail.com',
      metadata: { userId: 'u_42' },
    }),
  },
)

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

**PHP**

```php
<?php

$client = curl_init(
  'https://api.vexorpay.com/api/v1/payment-links/66f1c1e2c8a4b0d1f2e3a4b5/checkout',
);
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([
    'reference' => 'ORD-1001',
    'customer_email' => 'cliente@mail.com',
    'metadata' => ['userId' => 'u_42'],
  ]),
]);

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

**Python**

```python

response = requests.post(
    'https://api.vexorpay.com/api/v1/payment-links/66f1c1e2c8a4b0d1f2e3a4b5/checkout',
    headers={
        'Authorization': 'Bearer vxp_live_tu_clave',
        'Content-Type': 'application/json',
    },
    json={
        'reference': 'ORD-1001',
        'customer_email': 'cliente@mail.com',
        'metadata': {'userId': 'u_42'},
    },
)
data = response.json()
```

**Go**

```go
body, _ := json.Marshal(map[string]any{
    "reference":      "ORD-1001",
    "customer_email": "cliente@mail.com",
    "metadata":       map[string]any{"userId": "u_42"},
})

req, _ := http.NewRequest("POST", "https://api.vexorpay.com/api/v1/payment-links/66f1c1e2c8a4b0d1f2e3a4b5/checkout", 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

`200 OK`

```json
{
  "provider": "polar",
  "polarCheckoutId": "8f6c1d2e-...",
  "price_id": null,
  "seats": null,
  "checkoutUrl": "https://buy.polar.sh/polar_c_...",
  "expiresAt": "2026-01-02T13:00:00.000Z"
}
```

Redirige al cliente a `checkoutUrl` antes de `expiresAt`. La venta llega como orden de Polar ([`GET /api/v1/orders`](/docs/api/orders)). `price_id` es `null` en links de pago único y `seats` es `null` salvo en planes por asiento.

### Links recurrentes: checkout directo a un plan

En un link `recurring`, manda el `price_id` del plan (o nada para el default):

```bash
curl https://api.vexorpay.com/api/v1/payment-links/66f1c1e2c8a4b0d1f2e3a4b5/checkout \
  -X POST \
  -H "Authorization: Bearer vxp_live_tu_clave" \
  -H "Content-Type: application/json" \
  -d '{ "price_id": "66f1c1e2c8a4b0d1f2e3a4c1" }'
```

```json
{
  "provider": "polar",
  "polarCheckoutId": "8f6c1d2e-...",
  "price_id": "66f1c1e2c8a4b0d1f2e3a4c1",
  "seats": null,
  "checkoutUrl": "https://buy.polar.sh/polar_c_...",
  "expiresAt": "2026-01-02T13:00:00.000Z"
}
```

Cuando el comprador paga, Polar crea la suscripción y la cobra cada ciclo (webhooks `subscription.created` y `subscription.payment.completed`; ver [Suscripciones](/docs/api/subscriptions)).

## Errores

| Error | HTTP | Causa |
| --- | --- | --- |
| `link_not_found` | 404 | No existe ese link de pago, no está activo o es de otro modo (prueba/producción). |
| `price_not_found` | 404 | `price_id` no existe o no pertenece a ese link recurrente. |
| `validation_error` | 400 | `price_id` en un link de pago único, u otro dato inválido. |
| `invalid_request` | 400 | El body no es JSON válido. |
| `invalid_seats` | 400 | `seats` fuera de `minSeats`/`maxSeats` del plan, o `seats` en un plan de precio fijo. |
| `prices_required` | 400 | El link recurrente no tiene planes. |
| `polar_link_not_editable_yet` | 409 | El link todavía se está creando en Polar: 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_checkout_failed` | 502 | Polar no creó el checkout. Reintenta en unos minutos. |
