# Billing

> Manage workspace balance, credit top-ups, saved payment methods, and billing settings.

**URL:** https://inference.sh/docs/api/rest/billing
**Last updated:** 2026-10-11

---

Manage workspace balance, credit top-ups, saved payment methods, and billing settings.

All endpoints require authentication. Use API key scopes `billing:read` (view balance and settings) and `billing:write` (checkout, charges, and settings updates).

Transactional emails (balance alerts, invoices, subscription notices) link to team billing pages under `/settings/team/billing/...`. To manage product email preferences, use [Settings → Notifications](https://app.inference.sh/settings/notifications) (welcome and onboarding emails include an unsubscribe link to this page).

→ [Subscription](/docs/api/rest/subscription) — plan signup and Stripe subscription management

### Workspace settings URLs

In the web app, settings opens as a dialog over whichever page you're on. The active page is addressed by the URL **fragment** (`#settings/team/billing`, `#settings/team/keys`, …). Navigating inside settings changes only the fragment.

Use **path-style entry URLs** in docs, emails, and `success_url` / `cancel_url` fields: `https://app.inference.sh/settings/team/billing?session_id={CHECKOUT_SESSION_ID}`. The app redirects to `https://app.inference.sh/?session_id=…#settings/team/billing`, reads one-time query flags (`session_id`, `setup_session_id`, `success=1`, `addon_success=1`), then removes them from the URL while keeping the fragment. Legacy `/settings/billing` and `/settings/keys` shortcuts redirect to the team paths.

**Checkout started in the workspace** (top-up, subscription, add-on, saved card setup) returns to the page where checkout began: success adds the flag to that page's query and opens settings on the handler page; cancel returns to the exact starting URL (fragment included). The workspace always passes its own redirect URLs — API defaults such as `{origin}/settings/team/billing/subscription?success=1` still work through the `/settings/<path>` entry redirect.

**Organization billing:** When the selected team is billed by an organization, credit top-ups in the web app call `POST /billing/checkout` and `POST /billing/charge` as the **paying** team (the organization workspace) when the signed-in user may manage that workspace's billing. Success redirects target that payer's billing settings (`#settings/team/billing`). Users who can see member-team usage but cannot manage organization billing do not get a checkout session from these flows. See [Organizations — Billing](/docs/org/organizations#billing).

On the API, write endpoints in this document and in [Subscription](/docs/api/rest/subscription) require the **`manage_billing`** capability: team **owner** on a standalone team, or team **owner** on the org workspace when the org pays. Calling checkout, charge, subscription, or payment-method routes with an org **member** team in context returns **403**, even if you are an org admin on that member team — switch `X-Team-ID` (or your SDK team context) to the organization workspace (its team id matches `org_id` on member teams). Read endpoints (`GET /billing`, balance, settings, payment history) still work for member-team admins.

---

## Money units

| Context | Unit | Example |
|---------|------|---------|
| `GET /billing`, `GET /billing/balance` | **Microcents** (1 USD = 100,000,000) | `250000000` = $2.50 |
| `POST /billing/checkout`, `POST /billing/charge` body `amount` | **Cents** (1 USD = 100) | `500` = $5.00 minimum top-up |

---

## Get billing account

`GET /billing`

Returns the workspace's billing account with cached balance.

### Response

| Field | Type | Description |
|-------|------|-------------|
| `balance` | int64 | Current balance in microcents |
| `currency` | string | Currency code (for example `usd`) |
| `status` | string | Account status: `active`, `spend_locked`, `manual_hold` or `suspended` (see below) |

| `status` | Meaning |
|----------|---------|
| `active` | Spending is allowed. |
| `spend_locked` | A charge took the balance below the spend floor (`-credit_limit`). Runs are refused, free runs included. It clears itself when a credit (top-up, auto-recharge, voucher) takes the balance above the floor. |
| `manual_hold` | Held by the platform, for example after a payment dispute. Runs are refused until support lifts the hold. |
| `suspended` | Suspended for a payment, risk or compliance issue. Runs are refused until support resolves it. |

**Example:**

```bash
curl https://api.inference.sh/billing \
  -H "Authorization: Bearer inf_your_key"
```

---

## Get balance

`GET /billing/balance`

Recalculates balance from grants and usage, then returns the current value. Use this after checkout when status is `processing`, or when you need an authoritative balance.

### Response

```json
{
  "balance": 250000000
}
```

`balance` is in **microcents**.

---

## Service fee

`GET /billing/service-fee`

Returns the active service-fee configuration used for top-ups (default **5% + $0.30**). Call before checkout or a saved-card charge to show users the full total including fee and tax.

---

## Add credits

Two paths depending on whether a card is already on file.

| Method | Endpoint | When to use |
|--------|----------|-------------|
| **Stripe Checkout** | `POST /billing/checkout` | No saved card, user picks a payment method, or delayed methods (ACH, SEPA) |
| **Saved card** | `POST /billing/charge` | `GET /billing/settings` reports `has_payment_method: true` |

Both paths apply the same service fee and tax rules, then credit balance on success.

### Stripe Checkout

`POST /billing/checkout` — requires `billing:write`.

**Body:**

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `amount` | int64 | Yes | Credit to add, in **cents** (minimum **500** = $5) |
| `success_url` | string | Yes | Redirect URL after payment |
| `cancel_url` | string | Yes | Redirect URL if the user cancels |

**Response** — `PaymentRecordDTO` with `status: "pending"` and `session_url` for redirect. Line items include `credit_amount`, `service_fee`, and tax fields filled when checkout completes.

**Complete checkout:**

`POST /billing/checkout/success` — body: `{ "session_id": "<stripe_checkout_session_id>" }`.

| `status` | Meaning |
|----------|---------|
| `complete` | Balance credited now; `grant` included |
| `processing` | Delayed payment; balance credited when Stripe confirms (poll `GET /billing/balance`) |

Credits apply exactly once — the success URL and Stripe webhooks share the same completion path.

**Example:**

```bash
curl -X POST https://api.inference.sh/billing/checkout \
  -H "Authorization: Bearer inf_your_key" \
  -H "Content-Type: application/json" \
  -d '{
    "amount": 2000,
    "success_url": "https://app.inference.sh/settings/team/billing?success=1",
    "cancel_url": "https://app.inference.sh/settings/team/billing"
  }'
```

### Saved card charge

`POST /billing/charge` — requires `billing:write`.

**Body:**

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `amount` | int64 | Yes | Credit to add, in **cents** (minimum **500**) |

**Success response:**

```json
{
  "status": "complete",
  "grant": { }
}
```

Balance is credited immediately.

**Errors** (`400`, code `charge_failed`):

| Message | Cause |
|---------|--------|
| `no saved payment method` | Use checkout or save a card first |
| `minimum top-up amount is $5` | `amount` < 500 |
| `payment failed: ...` | Stripe declined the charge |

---

## Redeem voucher code

Redeem a promotional or gift code to add credits to the **current workspace**. In the web app, open [settings → workspace → billing](https://app.inference.sh/settings/team/billing) and use the **Redeem code** section (monospace input, case-normalized as you type).

`POST /billing/vouchers/redeem` — requires `billing:write`.

**Body:**

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `code` | string | Yes | Voucher code (whitespace trimmed; matching is case-insensitive) |

**Response** — `CreditGrantDTO` for the credited amount. `amount` and `remaining` are in **microcents**. Grant `type` is typically `voucher`; `notes` includes the code string.

**Example:**

```bash
curl -X POST https://api.inference.sh/billing/vouchers/redeem \
  -H "Authorization: Bearer inf_your_key" \
  -H "Content-Type: application/json" \
  -d '{"code": "PROMO-ABCD-EFGH"}'
```

After a successful redemption, poll `GET /billing/balance` or call `GET /credit-grants/active` (requires `billing:read`) to confirm the updated balance and grant expiry.

### Rate limits

| Limit | Window |
|-------|--------|
| 10 attempts per user | 1 minute |
| 30 attempts per IP | 1 minute |

When exceeded, the API returns **429** with code `rate_limited` and message `Too many redemption attempts. Please try again later.`

### Errors

Failed redemptions return **400** with code `redeem_failed` and one of these messages:

| Message | Cause |
|---------|--------|
| `invalid code` | Code not found |
| `this code is no longer active` | Code revoked or already used (single-use codes) |
| `this voucher is no longer active` | Voucher paused, expired, or exhausted |
| `this code is not yet valid` | Before the voucher `valid_from` time |
| `this code has expired` | After the voucher `valid_until` time |
| `this voucher has reached its maximum number of redemptions` | Campaign cap reached |
| `this code has reached its maximum number of uses` | Per-code use limit reached |
| `you have already redeemed this voucher` | Per-user limit reached |
| `your team has already redeemed this voucher` | Per-workspace limit reached |
| `this code is assigned to a different account` | Code is email-restricted |
| `this voucher is only available for new users` | The **paying** workspace already has a `topup` or `auto_recharge` credit grant (for organization billing, that is the org workspace—not the member team in `X-Team-ID`) |
| `failed to credit account` | Redemption recorded but grant creation failed (retry may be idempotent) |

Voucher administration (creating campaigns, generating codes, pausing vouchers) is internal and not exposed on the public REST API.

### Bounty rewards

Credits from bounty programs (for example, the app-builder program via `belt bounty claim app-builder`, form programs via `belt bounty claim <program>`, or survey answers via `belt feedback`) are granted as `CreditGrantDTO` entries with `type` `bounty` or `survey`. Grants expire after the program's `expiry_days` (typically 30 days). See [Bounties](/docs/api/rest/teams#bounties) and [CLI setup — App bounties](/docs/extend/cli-setup#app-bounties-cli).

---

## Credit grants

Prepaid balance is stored as **credit grants** (top-ups, vouchers, bounties, plan credits, and similar). Usage debits draw from active grants in FIFO order. Grant `amount` and `remaining` are in **microcents**.

| Endpoint | Scope | Description |
|----------|-------|-------------|
| `GET /credit-grants/active` | `billing:read` | Active grants with `remaining` > 0 |
| `GET /credit-grants/breakdown` | `billing:read` | Totals by grant `type`, plus grants expiring within 30 days |
| `GET /credit-grants` | `billing:read` | Paginated grant history (cursor) |

All three also need the **`view_billing`** capability on the workspace (admins and owners); plain members get **403** `team_role_required`.

**Organization billing:** Grants are stored on the **payer** workspace (the organization workspace when the org pays). Top-ups, voucher redemptions, and bounty claims made while `X-Team-ID` is a member workspace still credit the org's grants and debits allocate against those grants. New-user-only voucher codes use the same payer when deciding whether a prior top-up exists. If you administer a member team but cannot see the organization's billing, `GET /credit-grants/active` and `GET /credit-grants/breakdown` return empty results (same visibility rule as `GET /billing` balance for delegated billing). Use the organization workspace id as team context, or see [Organizations — Billing](/docs/org/organizations#billing).

---

## Billing settings

`GET /billing/settings` — requires `billing:read`.

Returns auto-recharge, alerts, invoice fields, and saved-card metadata.

| Field | Description |
|-------|-------------|
| `has_payment_method` | Whether instant top-up (`POST /billing/charge`) is available |
| `payment_method_label` | Display label (for example `Visa •••• 4242`) |
| `auto_recharge_enabled` | Auto top-up when balance drops below threshold |
| `spending_limit` | Workspace spending cap (microcents) |
| `low_balance_threshold` | Alert threshold (microcents) |
| `tax_id` | Business tax identifier for invoices |
| `tax_id_type` | Kind of `tax_id` as selected in the app (lowercase Stripe-style code such as `us_ein`, `de_stn`, `eu_vat`, or `other`; empty when unset) |

`POST /billing/settings` — requires `billing:write`. Send only fields to update (`BillingSettingsUpdateRequest`). Set `tax_id_type` to `"none"` or `""` to clear it.

**Validation errors** (`400`, code `validation_error`):

| Message (examples) | Cause |
|--------------------|--------|
| `low balance threshold must be at least $...` | `low_balance_threshold` below the platform minimum |
| `auto-recharge amount must be at least $...` | `auto_recharge_amount` too small |
| `auto-recharge threshold must be at least $...` | `auto_recharge_threshold` too small |
| `invalid tax_id_type "..."` | `tax_id_type` is not a supported lowercase Stripe-style code |

Org-billed workspaces share the payer's settings row (credit limit, thresholds, auto-recharge, and Stripe customer), not a separate member-team copy.

---

## Saved payment methods

| Endpoint | Method | Scope | Purpose |
|----------|--------|-------|---------|
| `/billing/payment-method/setup` | POST | `billing:write` | Stripe Checkout to save a card |
| `/billing/payment-method/success` | POST | `billing:write` | Complete setup (`session_id` in body) |
| `/billing/payment-method` | DELETE | `billing:write` | Remove saved card |

Setup body: `{ "success_url": "...", "cancel_url": "..." }`. Success body: `{ "session_id": "..." }` — returns `last4` and `brand`.

Users can also save a card during a balance checkout when Stripe shows “Save my info for future purchases.”

---

## Payment history

`GET /billing/payments` — requires `billing:read`.

Lists payment records for the workspace (checkout sessions, charges, and status).

---

## Insufficient balance

Before checking balance, the spend gate evaluates the [usage policy](/docs/org/usage-policy) for the run (for example the app on `POST /run`). A policy-blocked run returns **403** with `blocked_by_usage_policy`, not a paywall — adding credits cannot unblock it until an admin widens the policy.

When the policy allows the run but balance is too low, the API returns **402 Payment Required** (`payment_required`) with a message pointing to [settings → workspace → billing](https://app.inference.sh/settings/team/billing). This is separate from **plan limit** errors (`limit_exceeded`, `feature_not_available`) enforced by entitlements.

The belt and infsh CLIs surface billing URLs in error text for 402 responses.

### Free runs

A run that is certain to cost $0 does not need credits. When the balance would refuse the run, the API admits it anyway if the workspace has a saved payment method and its billing status is active. Without a saved payment method the API returns **402** with code `payment_method_required`:

```json
{
  "type": "https://api.inference.sh/errors/payment_method_required",
  "title": "Payment Required",
  "status": 402,
  "detail": "add a payment method to your workspace's billing settings to run free apps",
  "meta": { "billing_page": "/settings/team/billing/payments" }
}
```

Save a card under [settings → workspace → billing → payments](https://app.inference.sh/settings/team/billing/payments) (or with [`POST /billing/payment-method/setup`](#saved-payment-methods)) and retry. Nothing is charged to the card for a free run, and a free run places no pre-auth hold.

A workspace whose spending is locked, that has a manual hold, or that is suspended is refused free runs too, with `payment_required` and the same message as a paid run.

A run counts as free only when the app version's reviewed pricing settles at $0 for the input and function sent, whatever the app outputs and however long it runs. Every fee expression and the total must read only the input and the function, and the total must be $0 under every fee configuration. A version without reviewed pricing is charged nothing, so its runs are free. A $0 estimate is not enough: pricing that reads output, run time or `resource_fee` is not certain, and neither is a flow app (each of its steps is admitted and held on its own). Those runs follow the paid path.

Paid runs are unchanged: they need a balance above the spend floor, and the run's pre-auth hold reserves its estimated cost.

---

## Related

- [Usage API](/docs/api/rest/usage): workspace spend breakdown, usage summary, and CEL pricing preview
- [Subscription API](/docs/api/rest/subscription) — plans and recurring billing
- [Usage API](/docs/api/rest/usage) — aggregated usage summary and cost breakdown
- [Entitlements API](/docs/api/rest/entitlements) — plan limits and `GET /entitlements/usage`
- [Tasks API](/docs/api/rest/tasks) — usage-based app pricing reads `output_meta` internally
- [Extend pricing](/docs/extend/pricing) — CEL formulas for store apps
