# Venta de producto digital

Integración completa de punta a punta: link plantilla, checkout 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, sin SDK oficial**. Migración: <https://developers.vexorpay.com/docs/migracion-desde-el-sdk>

Fuente: <https://developers.staging.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 comprador recibe por email el recibo de su pago.

## 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 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
{
  "checkoutId": "ch_4f8a2c9d1e7b",
  "reference": "ORD-1001",
  "price_id": null,
  "checkoutUrl": "https://…/checkout/ch_4f8a2c9d1e7b",
  "expiresAt": null
}
```

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",
  "sandbox": false,
  "data": {
    "object": {
      "id": "67f1c1e2c8a4b0d1f2e3a4b5",
      "paymentLinkId": "66f1c1e2c8a4b0d1f2e3a4b5",
      "subscriptionId": null,
      "checkoutId": "ch_4f8a2c9d1e7b",
      "reference": "ORD-1001",
      "metadata": { "userId": "u_42" },
      "status": "paid",
      "billingReason": "one_time",
      "currency": "usd",
      "subtotalCents": 1900,
      "taxCents": 0,
      "totalCents": 1900,
      "feeCents": 190,
      "feeBreakdown": {
        "vexorpay": { "amountCents": 151, "percent": 2.7, "fixedCents": 100 },
        "extras": [
          { "type": "cross_border", "amountCents": 29, "percent": 1.5 },
          { "type": "fraud_prevention", "amountCents": 7, "percent": null },
          { "type": "authentication", "amountCents": 3, "percent": null }
        ]
      },
      "netCents": 1710,
      "refundedCents": 0,
      "refundableCents": 1900,
      "customerEmail": "cliente@mail.com",
      "customerName": "Ana Pérez",
      "productName": "Ebook UX en 30 días",
      "sandbox": false,
      "createdAt": "2026-01-02T12:05:00.000Z"
    }
  }
}
```

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. Busca tu pedido por `data.object.reference` (`ORD-1001`), confirma que `status` sea `paid` y recién entonces márcalo como pagado y envía el ebook por email. Si quieres confirmar contra la API, consulta `GET /api/v1/orders/{data.object.id}`.

`data.object` es la [venta](/docs/api/orders) completa: `reference` es la referencia del pedido que mandaste en el checkout, y `feeCents` (la comisión de vexorpay, con el procesamiento incluido) y `netCents` traen el desglose de lo que te queda.

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