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/transactions | List balance transactions |
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,
"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
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: 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
}'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}'| 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: Bearer sk_live_YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"amount_cents": 10000}'result = requests.post(
"https://api.thunderphone.com/v1/billing/top-up",
headers={"Authorization": "Bearer sk_live_YOUR_API_KEY"},
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: Bearer sk_live_YOUR_API_KEY"{
"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: Bearer sk_live_YOUR_API_KEY" \
-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.
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": {},
"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.