> ## Documentation Index
> Fetch the complete documentation index at: https://docs.thunderphone.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Billing

> Inspect your balance, top up, configure auto-reload, and list transactions.

ThunderPhone is prepaid. Each organization carries a USD balance that
decrements as calls are placed; if the balance reaches `$0.00`, inbound
calls are rejected and outbound calls return `402 Payment Required`.
These endpoints let you query balance state, top up via Stripe, and
configure automatic reloads.

## Endpoints

| Method  | Path                                 | Description                                      |
| ------- | ------------------------------------ | ------------------------------------------------ |
| `GET`   | `/v1/billing`                        | Get billing account state                        |
| `PATCH` | `/v1/billing`                        | Update auto-reload settings                      |
| `POST`  | `/v1/billing/top-up`                 | Create a top-up payment intent (one-time charge) |
| `POST`  | `/v1/billing/setup-intent`           | Create a Stripe SetupIntent for saving a card    |
| `POST`  | `/v1/billing/default-payment-method` | Set the default payment method                   |
| `GET`   | `/v1/billing/transactions`           | List balance transactions                        |

## Billing account object

```json theme={null}
{
  "balance_cents": 125000,
  "balance_usd": "1250.00",
  "currency": "usd",
  "auto_reload_enabled": true,
  "auto_reload_minimum_cents": 1000,
  "auto_reload_amount_cents": 5000,
  "auto_reload_monthly_limit_cents": null,
  "has_payment_method": true,
  "has_ever_topped_up": true
}
```

| Field                             | Type            | Description                                                                                    |
| --------------------------------- | --------------- | ---------------------------------------------------------------------------------------------- |
| `balance_cents`                   | integer         | Current balance in USD cents                                                                   |
| `balance_usd`                     | string          | Formatted, e.g. `"1250.00"`                                                                    |
| `currency`                        | string          | Always `usd`                                                                                   |
| `auto_reload_enabled`             | boolean         | Whether automatic top-ups are on                                                               |
| `auto_reload_minimum_cents`       | integer         | Balance threshold that triggers a reload                                                       |
| `auto_reload_amount_cents`        | integer         | Amount to charge on reload                                                                     |
| `auto_reload_monthly_limit_cents` | integer \| null | Cap on auto-reload spend per calendar month. `null` = no cap. Sending `0` is treated as `null` |
| `has_payment_method`              | boolean         | Derived — true iff a default Stripe payment method is set                                      |
| `has_ever_topped_up`              | boolean         | Derived — true once the account has at least one successful manual top-up or auto-reload       |

***

## Get billing account

<CodeGroup>
  ```bash cURL theme={null}
  curl https://api.thunderphone.com/v1/billing \
    -H "Authorization: Bearer sk_live_YOUR_API_KEY"
  ```
</CodeGroup>

Returns `200 OK` with a [Billing account object](#billing-account-object).

***

## Update auto-reload

Enable, disable, or reconfigure automatic balance reloads. A
payment method must be on file (`has_payment_method: true`) before
`auto_reload_enabled` can be set to `true` — otherwise the server
returns `422 Unprocessable Entity` with
`{"detail": "Auto-reload requires a default payment method. Create a setup intent first."}`.

<CodeGroup>
  ```bash Enable auto-reload theme={null}
  curl -X PATCH https://api.thunderphone.com/v1/billing \
    -H "Authorization: Bearer sk_live_YOUR_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{
      "auto_reload_enabled": true,
      "auto_reload_minimum_cents": 1000,
      "auto_reload_amount_cents": 5000,
      "auto_reload_monthly_limit_cents": 50000
    }'
  ```

  ```bash Disable auto-reload theme={null}
  curl -X PATCH https://api.thunderphone.com/v1/billing \
    -H "Authorization: Bearer sk_live_YOUR_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{"auto_reload_enabled": false}'
  ```
</CodeGroup>

| Field                             | Type                   | Required when enabling                           |
| --------------------------------- | ---------------------- | ------------------------------------------------ |
| `auto_reload_enabled`             | boolean                |                                                  |
| `auto_reload_minimum_cents`       | integer ≥ 0            | yes — triggers a reload once balance drops below |
| `auto_reload_amount_cents`        | integer > 0            | yes — amount charged on reload                   |
| `auto_reload_monthly_limit_cents` | integer ≥ 0, or `null` | no — `null` means unlimited                      |

Returns `200 OK` with the updated [Billing account object](#billing-account-object).

***

## Top up balance

Create a one-time charge to increase your balance. Uses the default
payment method unless `payment_method_id` is supplied.

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST https://api.thunderphone.com/v1/billing/top-up \
    -H "Authorization: Bearer sk_live_YOUR_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{"amount_cents": 10000}'
  ```

  ```python Python theme={null}
  result = requests.post(
      "https://api.thunderphone.com/v1/billing/top-up",
      headers={"Authorization": "Bearer sk_live_YOUR_API_KEY"},
      json={"amount_cents": 10000},
  ).json()
  ```
</CodeGroup>

| Field               | Type        | Required | Description                                                                  |
| ------------------- | ----------- | -------- | ---------------------------------------------------------------------------- |
| `amount_cents`      | integer ≥ 1 | yes      | USD cents. No server-enforced maximum; the dashboard suggests common amounts |
| `payment_method_id` | string      | no       | Stripe PM id. Defaults to the org's default                                  |

Returns `201 Created`:

```json theme={null}
{
  "payment_intent_id": "pi_abc123",
  "status":            "succeeded",
  "client_secret":     "pi_abc123_secret_..."
}
```

| Field               | Type   | Description                                                                                                          |
| ------------------- | ------ | -------------------------------------------------------------------------------------------------------------------- |
| `payment_intent_id` | string | Stripe PaymentIntent id                                                                                              |
| `status`            | string | Mirrors Stripe's own `PaymentIntent.status` — typically `succeeded`, `requires_action`, or `requires_payment_method` |
| `client_secret`     | string | Pass to Stripe.js / stripe-react on the client side if `status === "requires_action"` (3DS)                          |

When `status === "succeeded"` the balance is credited synchronously
from the request. When `status === "requires_action"`, complete the
3DS flow client-side and wait for the `payment_intent.succeeded`
webhook — then poll [`GET /v1/billing`](#get-billing-account) to
observe the credit.

### Errors

Validation problems (e.g. a missing or non-positive `amount_cents`)
return `400` with a standard DRF field-error body:

```json theme={null}
{ "amount_cents": ["Ensure this value is greater than or equal to 1."] }
```

Payment failures return a structured error body with a
machine-readable `code`, plus `message`/`detail`, a `retryable` hint,
and a `next_action` your client can act on:

```json 402 example theme={null}
{
  "code": "payment_authentication_required",
  "message": "This card requires additional verification. Please retry and complete authentication when prompted.",
  "detail": "This card requires additional verification. Please retry and complete authentication when prompted.",
  "retryable": true,
  "next_action": "authenticate_payment"
}
```

| Status | `code`                            | `retryable` | `next_action`           | Condition                                                                                                            |
| ------ | --------------------------------- | ----------- | ----------------------- | -------------------------------------------------------------------------------------------------------------------- |
| `402`  | `payment_authentication_required` | `true`      | `authenticate_payment`  | 3-D Secure / SCA required — complete authentication client-side with the PaymentIntent's `client_secret`, then retry |
| `402`  | `card_declined`                   | `false`     | `update_payment_method` | The bank declined the card (all decline reasons are collapsed into this one code)                                    |
| `422`  | `billing_request_invalid`         | `false`     | `update_payment_method` | Stripe rejected the request (bad parameters, unknown customer)                                                       |
| `503`  | `billing_provider_unavailable`    | `true`      | `retry_payment`         | Transient Stripe outage or rate limit; retry shortly                                                                 |
| `502`  | `billing_provider_error`          | `true`      | `retry_payment`         | Any other unexpected Stripe error                                                                                    |

These same coded errors are returned by the other billing write
endpoints when they hit Stripe (`/setup-intent`,
`/default-payment-method`).

***

## Add / save a payment method

Two-step flow: create a SetupIntent, confirm it client-side, then set
the resulting payment-method id as the org's default.

### 1. Create a SetupIntent

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST https://api.thunderphone.com/v1/billing/setup-intent \
    -H "Authorization: Bearer sk_live_YOUR_API_KEY"
  ```
</CodeGroup>

```json Response theme={null}
{
  "setup_intent_id": "seti_abc123",
  "client_secret": "seti_abc123_secret_...",
  "status": "requires_payment_method"
}
```

### 2. Confirm client-side with Stripe.js

See the [Stripe documentation](https://docs.stripe.com/js/setup_intents/confirm_card_setup)
for `stripe.confirmCardSetup()`. On success you receive a
`payment_method` id.

### 3. Mark the payment method as default

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST https://api.thunderphone.com/v1/billing/default-payment-method \
    -H "Authorization: Bearer sk_live_YOUR_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{"payment_method_id": "pm_abc123"}'
  ```
</CodeGroup>

Returns `200 OK` with the updated [Billing account object](#billing-account-object);
`has_payment_method` is now `true`.

***

## List transactions

Returns every ledger entry on the account — top-ups, per-call usage,
monthly number fees, and manual adjustments — newest first.

<CodeGroup>
  ```bash cURL theme={null}
  curl 'https://api.thunderphone.com/v1/billing/transactions?limit=200' \
    -H "Authorization: Bearer sk_live_YOUR_API_KEY"
  ```

  ```python Python theme={null}
  txns = requests.get(
      "https://api.thunderphone.com/v1/billing/transactions",
      headers={"Authorization": "Bearer sk_live_YOUR_API_KEY"},
      params={"limit": 200},
  ).json()
  ```
</CodeGroup>

### Query parameters

| Param        | Type     | Default | Description                                           |
| ------------ | -------- | ------- | ----------------------------------------------------- |
| `limit`      | integer  | 200     | 1–1000                                                |
| `offset`     | integer  | 0       |                                                       |
| `kind`       | string   |         | Filter to one [transaction kind](#transaction-object) |
| `start_date` | ISO 8601 |         | Filter on `created_at`                                |
| `end_date`   | ISO 8601 |         |                                                       |

The response is a **plain JSON array** of transaction objects (no
`{results, total}` envelope). Page forward by increasing `offset`
until you receive fewer rows than `limit`.

### Transaction object

```json theme={null}
{
  "id": 3421,
  "kind": "call_usage",
  "description": "Call billing: 2 minutes × $0.02",
  "amount_cents": -4,
  "amount_usd": "-0.04",
  "call_id": 987654321,
  "phone_number_id": 201,
  "phone_number_value": "+15551234567",
  "metadata": {},
  "created_at": "2026-04-20T18:25:06.201Z"
}
```

| Field                | Type            | Description                                                                                                   |
| -------------------- | --------------- | ------------------------------------------------------------------------------------------------------------- |
| `id`                 | integer         | Transaction id                                                                                                |
| `kind`               | string          | One of `manual_topup`, `auto_reload`, `call_usage`, `phone_number_monthly`, `conversion_report`, `adjustment` |
| `description`        | string          | Human-readable summary                                                                                        |
| `amount_cents`       | integer         | Negative for usage/fees, positive for credits                                                                 |
| `amount_usd`         | string          | Formatted signed amount                                                                                       |
| `call_id`            | integer \| null | Set on `call_usage` rows                                                                                      |
| `phone_number_id`    | integer \| null | Set on `phone_number_monthly` rows                                                                            |
| `phone_number_value` | string \| null  | E.164 for the number billed                                                                                   |
| `metadata`           | object          | Kind-specific details                                                                                         |
| `created_at`         | timestamp       |                                                                                                               |

***

## Stripe webhook

ThunderPhone receives Stripe webhooks at
`/v1/stripe/webhook` internally — you don't interact with this endpoint
directly. It processes `payment_intent.succeeded`,
`payment_intent.payment_failed`, and `setup_intent.succeeded` events to
update balances and mark payment methods ready.

***

## Related

<CardGroup cols={2}>
  <Card title="Outbound Calls" icon="arrow-up-right" href="/api-reference/outbound-calls">
    Outbound calls require positive balance.
  </Card>

  <Card title="Pricing" icon="credit-card" href="https://thunderphone.com/pricing">
    Per-minute pricing by product tier.
  </Card>
</CardGroup>
