# Venta de producto digital

Integración completa de punta a punta: link plantilla, checkout de Polar por pedido, webhook firmado, reconciliación y contabilidad.

> [!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/sell-digital-product>

Escenario: una web vende el ebook "UX en 30 días" a USD 19 y quiere que cada compra genere un pedido único (`ORD-xxxx`), se confirme por webhook y quede conciliada. El recibo y la factura se los envía Polar (el Merchant of Record) al comprador por email.

## 1. Crea la plantilla (una sola vez)

**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 '{ "amount": 19, "title": "Ebook UX en 30 días", "reference": "CATALOGO-UX" }'
```

**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({
    amount: 19,
    title: 'Ebook UX en 30 días',
    reference: 'CATALOGO-UX',
  }),
})

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([
    'amount' => 19,
    'title' => 'Ebook UX en 30 días',
    'reference' => 'CATALOGO-UX',
  ]),
]);

$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={
        'amount': 19,
        'title': 'Ebook UX en 30 días',
        'reference': 'CATALOGO-UX',
    },
)
data = response.json()
```

**Go**

```go
body, _ := json.Marshal(map[string]any{
    "amount":    19,
    "title":     "Ebook UX en 30 días",
    "reference": "CATALOGO-UX",
})

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()
```

Guardas el `id` del link, p. ej. `66f1c1e2c8a4b0d1f2e3a4b5`.

## 2. Alguien compra → checkout único por pedido

Cuando tu cliente inicia la compra, llamas al checkout con la referencia del pedido y el email del comprador. Cada llamada crea una sesión de checkout de Polar nueva (no usa `Idempotency-Key`), así que guarda la URL si el cliente vuelve a intentar con el mismo pedido:

**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:

```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 ese `checkoutUrl`. La venta recién existe cuando el cliente paga: llega como una **orden** (`GET /api/v1/orders`) y con el webhook `payment.completed`.

## 3. El servidor del vendedor recibe el webhook

Al confirmarse el pago, vexorpay hace `POST` a tu endpoint configurado en **Configuración → Webhooks**, con headers `vexorpay-Signature` y `vexorpay-Event-Id`:

```json
{
  "id": "evt_1001",
  "type": "payment.completed",
  "created": "2026-01-02T12:05:00.000Z",
  "data": {
    "charge": {
      "id": "67f1c1e2c8a4b0d1f2e3a4b5",
      "provider": "polar",
      "reference": "CATALOGO-UX",
      "amountCents": 1900,
      "currency": "usd",
      "status": "paid",
      "sandbox": false,
      "refundedCents": 0,
      "paidAt": "2026-01-02T12:05:00.000Z"
    },
    "payment_link": {
      "id": "66f1c1e2c8a4b0d1f2e3a4b5",
      "slug": "a1b2c3d4e5",
      "title": "Ebook UX en 30 días",
      "reference": "CATALOGO-UX"
    }
  }
}
```

Tu servidor:

1. **Verifica la firma** (HMAC-SHA256 del body crudo con tu secreto; comparación en tiempo constante).
2. **Deduplica por `id`** (guardas `evt_1001` como procesado).
3. Consulta la venta (`GET /api/v1/orders/{charge.id}`) para ver el email del comprador, el link y los montos, y recién entonces marca el pedido como pagado y envía el ebook por email.

`charge.id` es el id de la venta (la orden de Polar) y `charge.reference` es la referencia **del link**. Para asociar la venta con tu pedido usa el `customerEmail` y el `paymentLinkId` de la orden.

## 4. Verificación a demanda (por si se perdió el webhook)

Siempre puedes preguntar por la venta puntual:

**curl**

```bash
curl https://api.vexorpay.com/api/v1/orders/67f1c1e2c8a4b0d1f2e3a4b5 \
  -H "Authorization: Bearer vxp_live_tu_clave"
```

**Node.js**

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

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

**PHP**

```php
<?php

$client = curl_init('https://api.vexorpay.com/api/v1/orders/67f1c1e2c8a4b0d1f2e3a4b5');
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/orders/67f1c1e2c8a4b0d1f2e3a4b5',
    headers={'Authorization': 'Bearer vxp_live_tu_clave'},
)
data = response.json()
```

**Go**

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

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

O listar el histórico de eventos con el **mismo `evt_`** que te hubiera llegado por webhook:

**curl**

```bash
curl "https://api.vexorpay.com/api/v1/events?type=payment.completed&created[gte]=2026-01-02T00:00:00.000Z" \
  -H "Authorization: Bearer vxp_live_tu_clave"
```

**Node.js**

```js
const url = new URL('https://api.vexorpay.com/api/v1/events')
url.searchParams.set('type', 'payment.completed')
url.searchParams.set('created[gte]', '2026-01-02T00:00:00.000Z')

const res = await fetch(url, {
  headers: { Authorization: 'Bearer vxp_live_tu_clave' },
})

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

**PHP**

```php
<?php

$client = curl_init(
  'https://api.vexorpay.com/api/v1/events' .
    '?type=payment.completed' .
    '&created%5Bgte%5D=2026-01-02T00:00:00.000Z',
);
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/events',
    params={
        'type': 'payment.completed',
        'created[gte]': '2026-01-02T00:00:00.000Z',
    },
    headers={'Authorization': 'Bearer vxp_live_tu_clave'},
)
data = response.json()
```

**Go**

```go
req, _ := http.NewRequest("GET", "https://api.vexorpay.com/api/v1/events?type=payment.completed&created%5Bgte%5D=2026-01-02T00%3A00%3A00.000Z", nil)
req.Header.Set("Authorization", "Bearer vxp_live_tu_clave")

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

## 5. Saldo y contabilidad

**curl**

```bash
curl https://api.vexorpay.com/api/v1/balance \
  -H "Authorization: Bearer vxp_live_tu_clave"

curl "https://api.vexorpay.com/api/v1/orders?status=paid" \
  -H "Authorization: Bearer vxp_live_tu_clave"

curl "https://api.vexorpay.com/api/v1/payouts" \
  -H "Authorization: Bearer vxp_live_tu_clave"
```

**Node.js**

```js
const headers = { Authorization: 'Bearer vxp_live_tu_clave' }

const balance = (await fetch('https://api.vexorpay.com/api/v1/balance', {
  headers,
})).json()

const paid = (await fetch(
  'https://api.vexorpay.com/api/v1/orders?status=paid',
  { headers },
)).json()

const payouts = (await fetch('https://api.vexorpay.com/api/v1/payouts', {
  headers,
})).json()
```

**PHP**

```php
<?php

function get($url) {
  $client = curl_init($url);
  curl_setopt_array($client, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => ['Authorization: Bearer vxp_live_tu_clave'],
  ]);
  return json_decode(curl_exec($client), true);
}

$balance = get('https://api.vexorpay.com/api/v1/balance');
$paid = get('https://api.vexorpay.com/api/v1/orders?status=paid');
$payouts = get('https://api.vexorpay.com/api/v1/payouts');
```

**Python**

```python

headers = {'Authorization': 'Bearer vxp_live_tu_clave'}

balance = requests.get('https://api.vexorpay.com/api/v1/balance', headers=headers).json()
paid = requests.get(
    'https://api.vexorpay.com/api/v1/orders',
    params={'status': 'paid'},
    headers=headers,
).json()
payouts = requests.get('https://api.vexorpay.com/api/v1/payouts', headers=headers).json()
```

**Go**

```go
reqBalance, _ := http.NewRequest("GET", "https://api.vexorpay.com/api/v1/balance", nil)
reqBalance.Header.Set("Authorization", "Bearer vxp_live_tu_clave")
balance, _ := http.DefaultClient.Do(reqBalance)
defer balance.Body.Close()

reqPaid, _ := http.NewRequest("GET", "https://api.vexorpay.com/api/v1/orders?status=paid", nil)
reqPaid.Header.Set("Authorization", "Bearer vxp_live_tu_clave")
paid, _ := http.DefaultClient.Do(reqPaid)
defer paid.Body.Close()

reqPayouts, _ := http.NewRequest("GET", "https://api.vexorpay.com/api/v1/payouts", nil)
reqPayouts.Header.Set("Authorization", "Bearer vxp_live_tu_clave")
payouts, _ := http.DefaultClient.Do(reqPayouts)
defer payouts.Body.Close()
```

## Resumen del flujo

| Paso | Endpoint | Qué pasa |
| --- | --- | --- |
| 1 | `POST /api/v1/payment-links` | Creas la plantilla (una vez). |
| 2 | `POST /api/v1/payment-links/{id}/checkout` | Checkout de Polar por pedido + redirect. |
| 3 | Webhook `payment.completed` | Tu servidor confirma y entrega. |
| 4 | `GET /api/v1/orders/{id}`, `GET /api/v1/events` | Reconcilias si se perdió algo. |
| 5 | `GET /api/v1/balance`, `GET /api/v1/payouts`, `GET /api/v1/orders` | Contabilidad. |
