# クレジットを販売する

> API や AI サービスのための前払いクレジット：購入者が自分でチャージし、サーバーが呼び出しごとに消費。チャージから差し引くものはありません。

クレジットは、使った分だけ売るものなら何にでも合います：トークン、画像、分数、検索。購入者は暗号資産でパックを購入し、あなたは呼び出しごとにそこから消費し、クレジットがなくなれば購入者が自分のページでチャージします。自動で請求されることはなく — 支払うのは常に購入者自身です — 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**（1 パックの N 倍）— 固定パックだけでなく、$25 などでもチャージできるように。
3. **購入者ごとに顧客を用意** — ユーザーごとに、あなたの id で 1 つずつ作成：`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"}'
```

- 顧客は `cus_…` の id、メールアドレス、またはあなた独自の id（`external_id`）で指定します。複数の商品でクレジットを売る場合は `product` を指定してください。
- 消費はアトミックで、ゼロを下回ることはありません：同じクレジットに対する同時の 2 回は両方は通りません。不足は `insufficient_credits`（409）で、何も消費されません。
- リクエストの id を冪等キーとして渡してください。タイムアウトや障害の後、翌日であっても、再試行は最初の消費を返し、二重に消費しません。
- `action` はあなたのラベルです（`chat`、`image.hd`、`api/search`）。商品ページで、アクション別・顧客別の消費が集計されます。

## チャージページ

`top_up_url` は購入者専用の、毎回同じページです：残高と、あなたのチェックアウトのデザインでパックを表示し、購入者がパックを選ぶと通常の請求書を作成します。支払いが済むと、クレジットが一度だけ残高に加算されます。アプリ内や「クレジットがなくなりました」のエラーでリンクを表示するか、送ってください。購入者ポータルにも**チャージ**ボタンがあります。

## クレジットがなくなったら

| イベント | タイミング |
| --- | --- |
| `credits.low` | 残高が商品のしきい値まで下がった。1 回の到達につき 1 回：再び送られるのは、チャージでしきい値を上回った後だけ。 |
| `credits.depleted` | 残高がゼロになった。1 回の到達につき 1 回。 |

どちらも顧客を `data.object` に、さらに `data.product` と `data.balance` を含みます。しきい値が設定されメールがオンなら、購入者にはチャージのリンク付きのメールも 1 通届きます — 1 日に 1 回まで。テストモードでは、メールはあなたのチームにだけ届きます。

> **注:** 複数パックのクレジットは合算され、消費は古いパックから行われます。
