Open in
Billing
Inspect your prepaid balance, top up by card, configure auto-reload thresholds and amounts, and list every transaction behind your organization
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/payment-methods | List saved cards |
DELETE | /v1/billing/payment-methods/{payment_method_id} | Remove a saved card |
GET | /v1/billing/transactions | List balance transactions |
GET | /v1/billing/transactions/{transaction_id}/receipt | Get the receipt link for a top-up |
Billing account object
{
"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,
"auto_reload_status": "ok",
"auto_reload_next_retry_at": null,
"auto_reload_failed_amount_cents": null,
"auto_reload_failed_at": null,
"auto_reload_consecutive_declines": null,
"last_auto_reload": {
"amount_cents": 5000,
"created_at": "2026-09-20T14:02:11.482Z"
},
"auto_reload_month_spend_cents": 5000,
"average_daily_spend_cents_7d": 390,
"balance_runway_days": 320,
"last_top_up": {
"amount_cents": 5000,
"created_at": "2026-09-20T14:02:11.482Z",
"kind": "auto"
},
"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. Turned off automatically when a top-up is refunded or disputed; set it back to true to resume |
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 |
auto_reload_status | string | Read-only. off (auto-reload disabled), ok, retrying (the default card was declined, or Stripe could not charge it at all, in which case auto_reload_failure_code is payment_method_unusable; the next attempt waits out a 1h / 6h / 24h backoff), stopped (the bank refused the default card outright, or the card was declined 5 times in a row, in which case auto_reload_failure_code is repeated_declines; it is not retried until you set another default card, the card's details are updated, or a manual top-up succeeds), or capped (this month's auto-reloads plus auto_reload_amount_cents would exceed auto_reload_monthly_limit_cents, so no auto-reload runs until the next calendar month in UTC or a higher limit) |
auto_reload_next_retry_at | string | null | Read-only. ISO 8601 end of the current backoff while retrying. Can be in the past: the retry runs on the next call after it, including a call attempt that is rejected because the balance is below zero |
auto_reload_failed_amount_cents | integer | null | Read-only. Amount the declined auto-reload tried to charge, while retrying or stopped |
auto_reload_failed_at | string | null | Read-only. ISO 8601 time of the latest decline, while retrying or stopped |
auto_reload_consecutive_declines | integer | null | Read-only. Failed auto-reloads in a row on the current default card, while retrying or stopped; null otherwise |
last_auto_reload | object | null | Read-only. amount_cents and created_at of the newest successful auto-reload, or null before the first one |
auto_reload_month_spend_cents | integer | Read-only. Auto-reload credits this calendar month (UTC), as auto_reload_monthly_limit_cents counts them |
average_daily_spend_cents_7d | integer | null | Read-only. Usage charges (calls, phone-number rent, conversion reports, voice samples) over the last 7 days, averaged per day of history (7 days, or the account's age if younger, at least 1 day), rounded. Top-ups, auto-reloads and adjustments are excluded. null when there was no usage in that window |
balance_runway_days | integer | null | Read-only. Whole days balance_cents lasts at the 7-day average (0 means less than a day). null when the balance is zero or negative or there was no recent usage |
last_top_up | object | null | Read-only. amount_cents, created_at and kind (manual or auto) of the newest successful top-up, or null before the first one |
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
curl https://api.thunderphone.com/v1/billing \
-H "Authorization: Bearer sk_live_YOUR_API_KEY"Returns 200 OK with a 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."}.
curl -X PATCH https://api.thunderphone.com/v1/billing \
-H "Authorization: Token YOUR_SESSION_TOKEN" \
-H "X-ThunderPhone-Org: 42" \
-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
}'curl -X PATCH https://api.thunderphone.com/v1/billing \
-H "Authorization: Token YOUR_SESSION_TOKEN" \
-H "X-ThunderPhone-Org: 42" \
-H "Content-Type: application/json" \
-d '{"auto_reload_enabled": false}'| 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.
Top up balance
Create a one-time charge to increase your balance. Uses the default
payment method unless payment_method_id is supplied.
curl -X POST https://api.thunderphone.com/v1/billing/top-up \
-H "Authorization: Token YOUR_SESSION_TOKEN" \
-H "X-ThunderPhone-Org: 42" \
-H "Content-Type: application/json" \
-d '{"amount_cents": 10000}'result = requests.post(
"https://api.thunderphone.com/v1/billing/top-up",
headers={"Authorization": "Token YOUR_SESSION_TOKEN", "X-ThunderPhone-Org": "42"},
json={"amount_cents": 10000},
).json()| 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:
{
"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 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:
{ "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:
{
"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
curl -X POST https://api.thunderphone.com/v1/billing/setup-intent \
-H "Authorization: Token YOUR_SESSION_TOKEN" \
-H "X-ThunderPhone-Org: 42"{
"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
for stripe.confirmCardSetup(). On success you receive a
payment_method id.
3. Mark the payment method as default
curl -X POST https://api.thunderphone.com/v1/billing/default-payment-method \
-H "Authorization: Token YOUR_SESSION_TOKEN" \
-H "X-ThunderPhone-Org: 42" \
-H "Content-Type: application/json" \
-d '{"payment_method_id": "pm_abc123"}'Returns 200 OK with the updated Billing account object;
has_payment_method is now true.
To switch the default to a card that is already saved, call this endpoint with that card's id from List saved cards.
List saved cards
Returns every card saved on the organization, the default first and the rest newest first. Top-ups and auto-reloads charge the default card. Requires the admin or owner role.
curl https://api.thunderphone.com/v1/billing/payment-methods \
-H "Authorization: Bearer sk_live_YOUR_API_KEY"{
"payment_methods": [
{
"id": "pm_abc123",
"brand": "visa",
"last4": "4242",
"exp_month": 4,
"exp_year": 2031,
"is_default": true
},
{
"id": "pm_def456",
"brand": "mastercard",
"last4": "4444",
"exp_month": 12,
"exp_year": 2030,
"is_default": false
}
]
}An organization that has never saved a card gets an empty list.
Remove a saved card
Detaches the card from the organization. This cannot be undone; save the card again to use it later. Requires the admin or owner role.
curl -X DELETE https://api.thunderphone.com/v1/billing/payment-methods/pm_def456 \
-H "Authorization: Token YOUR_SESSION_TOKEN" \
-H "X-ThunderPhone-Org: 42"Returns 204 No Content.
| Status | code | Meaning |
|---|---|---|
404 | — | No saved card with that id on this organization |
409 | default_payment_method | The card is the default. Set another card as default first |
409 | payment_method_in_use | An auto-reload is charging this card right now. Retry shortly |
List transactions
Returns every ledger entry on the account — top-ups, per-call usage, monthly number fees, and manual adjustments — newest first.
curl 'https://api.thunderphone.com/v1/billing/transactions?limit=200' \
-H "Authorization: Bearer sk_live_YOUR_API_KEY"txns = requests.get(
"https://api.thunderphone.com/v1/billing/transactions",
headers={"Authorization": "Bearer sk_live_YOUR_API_KEY"},
params={"limit": 200},
).json()Query parameters
| Param | Type | Default | Description |
|---|---|---|---|
limit | integer | 200 | 1–1000 |
offset | integer | 0 | |
kind | string | Filter to one transaction kind | |
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
{
"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": {},
"has_receipt": false,
"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 |
has_receipt | boolean | true on card-funded manual_topup and auto_reload credits. Get a receipt for these rows |
created_at | timestamp |
Get a receipt
Returns a link to the payment receipt for a top-up or automatic top-up,
for rows where has_receipt is true. The receipt is hosted by our
payment processor, Stripe, and shows the amount, date, and card charged.
Owners and admins can fetch it; other members get 403, because the receipt shows the card brand and last four digits.
curl https://api.thunderphone.com/v1/billing/transactions/3398/receipt \
-H "Authorization: Bearer sk_live_YOUR_API_KEY"receipt = requests.get(
"https://api.thunderphone.com/v1/billing/transactions/3398/receipt",
headers={"Authorization": "Bearer sk_live_YOUR_API_KEY"},
).json()
print(receipt["receipt_url"]){
"receipt_url": "https://pay.stripe.com/receipts/payment/..."
}| Status | code | Meaning |
|---|---|---|
404 | — | No transaction with that id on this organization, or the transaction has no receipt |
404 | receipt_unavailable | The payment has no receipt yet. Try again later |
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.