> ## Documentation Index
> Fetch the complete documentation index at: https://paperplane-justin-winter-s-projects.vercel.app/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Credits

> Prepaid balances that pay for letters without a card at every checkout.

## What credits are

Credits are a prepaid balance that pays for letters. `POST /v1/orders` with a `credit_code` redeems from the balance instead of charging a card. There's no account to manage — the code *is* the balance.

## The two-part balance (CARD Act)

Under the federal CARD Act, purchased stored value must stay spendable for at least 5 years. Several states are stricter. So a credit account splits into two buckets:

| Bucket        | Expiry    | Spent when  |
| ------------- | --------- | ----------- |
| `face_cents`  | 5 years   | after bonus |
| `bonus_cents` | 12 months | first       |

`GET /v1/credits/{code}` returns both, plus `spendable`:

```bash theme={null}
curl https://sendpaperplane.com/v1/credits/pp-1a2b-3c4d-5e6f
```

```json theme={null}
{
  "status": "ok",
  "code": "pp-1a2b-3c4d-5e6f",
  "balance_cents": 5500,
  "face_cents": 5000,
  "bonus_cents": 500,
  "bonus_expires_at": "...",
  "face_expires_at": "..."
}
```

`balance_cents` is the total spendable now (`face_cents` + unexpired `bonus_cents`) — the number to show a caller checking their balance.

## Buying credits

`POST /v1/credits/checkout` mints a Stripe checkout session. The credit code is **emailed** once payment settles, regardless of which mode created the session.

By default (`checkout_ui_mode: "hosted"`, or the field omitted entirely) it returns a Stripe-hosted checkout URL to redirect to:

```json theme={null}
{
  "status": "ok",
  "checkout_ui_mode": "hosted",
  "payment_url": "https://checkout.stripe.com/..."
}
```

Pass `checkout_ui_mode: "embedded"` and it instead returns a Stripe Embedded Checkout `payment_client_secret` to mount in place with `initEmbeddedCheckout` — for a page (like paperplane's own `/embed` widget) that can't redirect or open a new tab. The response is a discriminated union keyed on `checkout_ui_mode`: exactly one of `payment_url` / `payment_client_secret` is ever present, and the field itself says unambiguously which:

```json theme={null}
{
  "status": "ok",
  "checkout_ui_mode": "embedded",
  "payment_client_secret": "cs_test_..._secret_..."
}
```

<Note>
  `POST /v1/credits/checkout` always requires Stripe to be configured — there is no `sandbox` flag on this endpoint, and it fails closed with `503 payments_not_configured` if `STRIPE_SECRET_KEY` is unset, regardless of what else the deployment is doing in sandbox. A `credit_code` on a **sandbox order**, by contrast, is never actually charged or even looked up — sandbox orders skip payment entirely, so any code (real, fake, or omitted) is accepted. See [Sandbox mode](/docs/sandbox).
</Note>

## Codes

* Format `pp-xxxx-xxxx-xxxx`, case-insensitive (we lower-case on input).
* Redeemed at order creation — pass `credit_code` alongside your letter.

## When to prefer credits

* High-volume or recurring senders who don't want a card prompt per letter.
* Budget controls — top up a fixed amount and it can't be exceeded.
* Gifting or distributing "send N letters" to other people.
