# Vender créditos

> Créditos prepagados para una API o un servicio de IA: los compradores recargan por su cuenta y tu servidor gasta por llamada. De una recarga no se descuenta nada.

Los créditos sirven para todo lo que se vende por uso: tokens, imágenes, minutos, búsquedas. El comprador paga un paquete en cripto; tú gastas de él en cada llamada; cuando se acaban los créditos, recarga en su propia página. Nada se cobra automáticamente (el comprador siempre paga él mismo) y Perly no se queda nada de la recarga.

## Configuración

1. **Crea un producto y sus paquetes** — Dashboard → Products → New, luego New price → **Credit pack**: por ejemplo, 1 000 créditos por $10, 5 000 por $40. O `pay.prices.create({ product, model: "credit_pack", unit_amount: 1000, credits: 1000 })`.
2. **Elige cuándo se entera el comprador de su saldo** — En la página del producto, en **Credits**: el umbral de aviso, un correo al comprador y **Any amount** (N de un paquete), para que el comprador pueda recargar $25 y no solo un paquete fijo.
3. **Da a cada comprador un cliente** — Crea uno por usuario con tu id: `pay.customers.create({ email, external_id: user.id })`. A partir de ahí, el SDK acepta ese id.

## Gastar, consultar, recargar

**Node**

```js
import { PerlyPay, InvalidRequestError } from "perly-pay";
const pay = new PerlyPay(process.env.PERLY_PAY_SECRET_KEY);

// Before the work: enough left?
const { balance, top_up_url } = await pay.credits.balance(user.id);

// After it: spend. The same idempotencyKey never spends twice.
try {
  await pay.credits.consume(user.id, 3, { action: "chat", idempotencyKey: request.id });
} catch (e) {
  if (e instanceof InvalidRequestError && e.code === "insufficient_credits") {
    return { error: "Out of credits", topUp: await pay.credits.topUpUrl(user.id) };
  }
  throw e;
}
```

**PHP**

```php
$perly = new \Perly\Pay\PerlyPay(getenv('PERLY_PAY_SECRET_KEY'));

$left = $perly->credits->balance($user->id)['balance'];

try {
    $perly->credits->consume($user->id, 3, ['action' => 'chat', 'idempotency_key' => $requestId]);
} catch (\Perly\Pay\Exception\InvalidRequestException $e) {
    if ($e->getErrorCode() === 'insufficient_credits') {
        header('Location: ' . $perly->credits->topUpUrl($user->id));
    }
}
```

**curl**

```bash
curl https://pay.perly.io/api/v1/credits/consume \
  -H "Authorization: Bearer sk_test_…" \
  -H "Idempotency-Key: req_8f2c" \
  -d '{"customer_external_id":"user_42","credits":3,"action":"chat"}'
```

- El cliente es un id `cus_…`, un correo o tu propio id para él (`external_id`). Indica `product` si vendes créditos para varios productos.
- El gasto es atómico y nunca baja de cero: dos gastos simultáneos no pasan con los mismos créditos. Si no alcanza, devuelve `insufficient_credits` (409) y no se gasta nada.
- Pasa el id de tu solicitud como clave de idempotencia. Un reintento (tras un timeout, un fallo, incluso al día siguiente) devuelve el primer gasto en vez de gastar otra vez.
- `action` es tu etiqueta (`chat`, `image.hd`, `api/search`). La página del producto suma el gasto por acción y por cliente.

## La página de recarga

`top_up_url` es la página propia del comprador, siempre la misma: muestra el saldo y tus paquetes con el aspecto de tu página de pago, y abre una factura normal cuando el comprador elige un paquete. En cuanto se paga, los créditos se suman al saldo una sola vez. Muestra el enlace en tu app o en el error de «sin créditos», o envíalo: el portal del comprador también tiene un botón **Recargar**.

## Cuando se acaban los créditos

| Evento | Cuándo |
| --- | --- |
| `credits.low` | El saldo bajó al umbral del producto. Una vez por cruce: vuelve a llegar solo después de que una recarga lo suba por encima. |
| `credits.depleted` | El saldo llegó a cero. Una vez por cruce. |

Ambos llevan el cliente como `data.object`, además de `data.product` y `data.balance`. Si hay umbral y el correo está activado, el comprador recibe también un correo con el enlace de recarga, como mucho una vez al día. En modo de prueba, los correos solo van a tu equipo.

> **Nota:** Los créditos de varios paquetes se suman; el gasto usa primero el paquete más antiguo.
