# Sprzedaż kredytów

> Przedpłacone kredyty dla API lub usługi AI: kupujący doładowują sami, Twój serwer zużywa je przy każdym wywołaniu. Z doładowania nic nie pobieramy.

Kredyty pasują do wszystkiego, co sprzedaje się za użycie: tokeny, obrazy, minuty, wyszukiwania. Kupujący płaci za pakiet kryptowalutą; Ty zużywasz z niego przy każdym wywołaniu; gdy kredyty się kończą, doładowuje je na własnej stronie. Nic nie jest pobierane automatycznie — kupujący zawsze płaci sam — a Perly nic nie bierze od doładowania.

## Konfiguracja

1. **Utwórz produkt i pakiety** — Dashboard → Products → New, potem New price → **Credit pack**: na przykład 1000 kredytów za $10, 5000 za $40. Albo `pay.prices.create({ product, model: "credit_pack", unit_amount: 1000, credits: 1000 })`.
2. **Wybierz, kiedy kupujący dowie się o saldzie** — Na stronie produktu, w sekcji **Credits**: próg ostrzeżenia, e-mail do kupującego i **Any amount** — N jednego pakietu, żeby kupujący mógł doładować za $25, a nie tylko stałym pakietem.
3. **Daj każdemu kupującemu klienta** — Utwórz po jednym na użytkownika z Twoim id: `pay.customers.create({ email, external_id: user.id })`. Potem SDK przyjmuje to id.

## Zużyj, sprawdź, doładuj

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

- Klient to id `cus_…`, e-mail albo Twoje własne id dla niego (`external_id`). Podaj `product`, jeśli sprzedajesz kredyty do kilku produktów.
- Zużycie jest atomowe i nigdy nie schodzi poniżej zera: dwa równoczesne nie przejdą na tych samych kredytach. Brak to `insufficient_credits` (409) i nic nie zostało zużyte.
- Przekazuj id swojego żądania jako klucz idempotencji. Ponowienie — po timeoucie, awarii, nawet następnego dnia — zwróci pierwsze zużycie, zamiast zużyć ponownie.
- `action` to Twoja etykieta (`chat`, `image.hd`, `api/search`). Strona produktu sumuje zużycie według akcji i według klienta.

## Strona doładowania

`top_up_url` to własna strona kupującego, za każdym razem ta sama: pokazuje saldo i Twoje pakiety w wyglądzie Twojej kasy i otwiera zwykłą fakturę, gdy kupujący wybierze pakiet. Gdy tylko zostanie opłacona, kredyty raz trafiają na saldo. Pokaż link w aplikacji, w błędzie „kredyty się skończyły” albo go wyślij — w portalu kupującego jest też przycisk **Doładuj**.

## Gdy kredyty się kończą

| Zdarzenie | Kiedy |
| --- | --- |
| `credits.low` | Saldo spadło do progu produktu. Raz na przekroczenie: ponownie dopiero, gdy doładowanie podniesie je wyżej. |
| `credits.depleted` | Saldo doszło do zera. Raz na przekroczenie. |

Oba niosą klienta jako `data.object` oraz `data.product` i `data.balance`. Jeśli próg jest ustawiony i e-mail włączony, kupujący dostaje też jeden e-mail z linkiem do doładowania — nie częściej niż raz dziennie. W trybie testowym e-maile trafiają tylko do Twojego zespołu.

> **Uwaga:** Kredyty z kilku pakietów się sumują; zużycie bierze najpierw najstarszy pakiet.
