# Продаж кредитів

> Передплачені кредити для API чи AI-сервісу: покупці поповнюють самі, ваш сервер витрачає по виклику. 0% з кожного поповнення.

Кредити пасують усьому, що продають за використання: токени, зображення, хвилини, пошуки. Покупець платить за пакет у криптовалюті; ви витрачаєте з нього на кожен виклик; коли кредити закінчуються, він поповнює на власній сторінці. Автоматично нічого не списується — покупець завжди платить сам, — і Perly нічого не бере з поповнення.

## Налаштування

1. **Створіть продукт і пакети** — Dashboard → Products → New, далі New price → **Credit pack**: наприклад, 1 000 кредитів за $10, 5 000 за $40. Або `pay.prices.create({ product, model: "credit_pack", unit_amount: 1000, credits: 1000 })`.
2. **Оберіть, коли покупець дізнається про залишок** — На сторінці продукту, у розділі **Credits**: поріг попередження, лист покупцеві та **Any amount** — N одного пакета, щоб покупець міг поповнити на $25, а не лише фіксованим пакетом.
3. **Дайте кожному покупцеві клієнта** — Створіть по одному на користувача з вашим id: `pay.customers.create({ email, external_id: user.id })`. Далі SDK приймає цей id.

## Витратити, перевірити, поповнити

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

- Клієнт — це id `cus_…`, пошта або ваш власний id для нього (`external_id`). Вкажіть `product`, якщо продаєте кредити на кілька продуктів.
- Витрата атомарна й ніколи не йде нижче нуля: дві одночасні не пройдуть на тих самих кредитах. Нестача — це `insufficient_credits` (409), і нічого не витрачено.
- Передавайте id свого запиту як ключ ідемпотентності. Повтор — після тайм-ауту, збою, навіть наступного дня — поверне першу витрату, а не витратить ще раз.
- `action` — ваша мітка (`chat`, `image.hd`, `api/search`). Сторінка продукту підсумовує витрати за дією та за клієнтом.

## Сторінка поповнення

`top_up_url` — власна сторінка покупця, щоразу та сама: вона показує залишок і ваші пакети у вигляді вашого чекауту та відкриває звичайний рахунок, коли покупець обирає пакет. Щойно його сплачено, кредити один раз зараховуються на баланс. Покажіть посилання у застосунку, у помилці «кредити закінчилися» або надішліть його — у порталі покупця також є кнопка **Поповнити**.

## Коли кредити закінчуються

| Подія | Коли |
| --- | --- |
| `credits.low` | Баланс опустився до порогу продукту. Раз за перетин: знову — лише після того, як поповнення підніме його вище. |
| `credits.depleted` | Баланс дійшов до нуля. Раз за перетин. |

Обидві несуть клієнта як `data.object`, а також `data.product` і `data.balance`. Якщо поріг задано й лист увімкнено, покупець отримує ще й один лист із посиланням на поповнення — не частіше разу на день. У тестовому режимі листи йдуть лише вашій команді.

> **Примітка:** Кредити з кількох пакетів додаються; витрата бере найстаріший пакет першим.
