# Vendre des crédits

> Des crédits prépayés pour une API ou un service d’IA : les acheteurs rechargent eux-mêmes, votre serveur consomme appel par appel. Rien n’est prélevé sur une recharge.

Les crédits conviennent à tout ce qui se vend à l’usage : jetons, images, minutes, recherches. L’acheteur paie un pack en crypto ; vous y puisez à chaque appel ; quand il s’épuise, il recharge sur sa propre page. Rien n’est jamais prélevé automatiquement — l’acheteur paie toujours lui-même — et Perly ne prend rien sur une recharge.

## Mise en place

1. **Créez un produit et ses packs** — Dashboard → Products → New, puis New price → **Credit pack** : par exemple 1 000 crédits pour 10 $, 5 000 pour 40 $. Ou `pay.prices.create({ product, model: "credit_pack", unit_amount: 1000, credits: 1000 })`.
2. **Choisissez quand l’acheteur apprend que son solde baisse** — Sur la page du produit, sous **Credits** : le seuil d’alerte, l’e-mail à l’acheteur et **Any amount** — N fois un pack, pour qu’un acheteur puisse recharger 25 $ au lieu d’un pack fixe.
3. **Donnez un client à chaque acheteur** — Créez-en un par utilisateur avec votre propre id : `pay.customers.create({ email, external_id: user.id })`. Ensuite, le SDK accepte cet id.

## Consommer, vérifier, recharger

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

- Le client est un id `cus_…`, un e-mail, ou votre propre id pour lui (`external_id`). Précisez un `product` si vous vendez des crédits pour plusieurs produits.
- Une consommation est atomique et ne passe jamais sous zéro : deux en même temps ne passent jamais toutes les deux sur les mêmes crédits. Un solde insuffisant donne `insufficient_credits` (409), et rien n’est consommé.
- Envoyez l’id de votre requête comme clé d’idempotence. Une nouvelle tentative — après un timeout, un plantage, même le lendemain — renvoie la première consommation au lieu de consommer à nouveau.
- `action` est votre libellé (`chat`, `image.hd`, `api/search`). La page du produit totalise les consommations par action et par client.

## La page de recharge

`top_up_url` est la page propre à l’acheteur, toujours la même : elle montre ce qui reste et vos packs, au style de votre caisse, et ouvre une facture normale quand il en choisit un. Une fois payée, les crédits arrivent une seule fois sur son solde. Mettez le lien dans votre app, dans une erreur « plus de crédits », ou envoyez-le — l’espace acheteur a aussi un bouton **Recharger**.

## Quand les crédits s’épuisent

| Événement | Quand |
| --- | --- |
| `credits.low` | Le solde est descendu au seuil d’alerte du produit. Une fois par franchissement : il ne revient qu’après qu’une recharge l’a fait remonter au-dessus. |
| `credits.depleted` | Le solde a atteint zéro. Une fois par franchissement. |

Les deux portent le client comme `data.object`, avec `data.product` et `data.balance`. Si le seuil est défini et l’e-mail activé, l’acheteur reçoit aussi un e-mail avec son lien de recharge — au plus un par jour. En mode test, les e-mails ne vont qu’à votre équipe.

> **Note:** Les crédits de plusieurs packs s’additionnent ; une consommation puise d’abord dans le pack le plus ancien.
