# Vendere crediti

> Crediti prepagati per un’API o un servizio di AI: gli acquirenti ricaricano da soli e il tuo server consuma a chiamata. Dalla ricarica non tratteniamo nulla.

I crediti vanno bene per tutto ciò che si vende a consumo: token, immagini, minuti, ricerche. L’acquirente paga un pacchetto in crypto; tu consumi da lì a ogni chiamata; quando i crediti finiscono, ricarica dalla sua pagina. Non viene mai addebitato nulla in automatico (l’acquirente paga sempre di persona) e Perly non trattiene nulla della ricarica.

## Configurazione

1. **Crea un prodotto e i suoi pacchetti** — Dashboard → Products → New, poi New price → **Credit pack**: per esempio 1.000 crediti per $10, 5.000 per $40. Oppure `pay.prices.create({ product, model: "credit_pack", unit_amount: 1000, credits: 1000 })`.
2. **Scegli quando l’acquirente scopre il suo saldo** — Nella pagina del prodotto, sotto **Credits**: la soglia di avviso, un’email all’acquirente e **Any amount** (N di un pacchetto), così l’acquirente può ricaricare $25 e non solo un pacchetto fisso.
3. **Dai a ogni acquirente un cliente** — Creane uno per utente con il tuo id: `pay.customers.create({ email, external_id: user.id })`. Da lì l’SDK accetta quell’id.

## Consumare, consultare, ricaricare

**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"}'
```

- Il cliente è un id `cus_…`, un’email o il tuo id per lui (`external_id`). Indica `product` se vendi crediti per più prodotti.
- Il consumo è atomico e non scende mai sotto zero: due consumi simultanei non passano con gli stessi crediti. Se non bastano, restituisce `insufficient_credits` (409) e non viene consumato nulla.
- Passa l’id della tua richiesta come chiave di idempotenza. Un nuovo tentativo (dopo un timeout, un crash, perfino il giorno dopo) restituisce il primo consumo invece di consumare di nuovo.
- `action` è la tua etichetta (`chat`, `image.hd`, `api/search`). La pagina del prodotto somma il consumo per azione e per cliente.

## La pagina di ricarica

`top_up_url` è la pagina dell’acquirente, sempre la stessa: mostra il saldo e i tuoi pacchetti con l’aspetto del tuo checkout, e apre una normale fattura quando l’acquirente sceglie un pacchetto. Appena è pagata, i crediti si aggiungono al saldo una sola volta. Mostra il link nella tua app o nell’errore «crediti esauriti», oppure invialo: anche il portale acquirenti ha un pulsante **Ricarica**.

## Quando i crediti finiscono

| Evento | Quando |
| --- | --- |
| `credits.low` | Il saldo è sceso alla soglia del prodotto. Una volta per attraversamento: torna solo dopo che una ricarica lo ha riportato sopra. |
| `credits.depleted` | Il saldo è arrivato a zero. Una volta per attraversamento. |

Entrambi portano il cliente come `data.object`, più `data.product` e `data.balance`. Se c’è una soglia e l’email è attiva, l’acquirente riceve anche un’email con il link di ricarica, al massimo una volta al giorno. In modalità test le email vanno solo al tuo team.

> **Nota:** I crediti di più pacchetti si sommano; il consumo usa prima il pacchetto più vecchio.
