ThunderPhone 2.0 is live.Self-serve, from 2¢/min.Read the announcement

Billing

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

MethodPathDescription
GET/v1/billingGet billing account state
PATCH/v1/billingUpdate auto-reload settings
POST/v1/billing/top-upCreate a top-up payment intent (one-time charge)
POST/v1/billing/setup-intentCreate a Stripe SetupIntent for saving a card
POST/v1/billing/default-payment-methodSet the default payment method
GET/v1/billing/transactionsList 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
}
FieldTypeDescription
balance_centsintegerCurrent balance in USD cents
balance_usdstringFormatted, e.g. "1250.00"
currencystringAlways usd
auto_reload_enabledbooleanWhether automatic top-ups are on
auto_reload_minimum_centsintegerBalance threshold that triggers a reload
auto_reload_amount_centsintegerAmount to charge on reload
auto_reload_monthly_limit_centsinteger | nullCap on auto-reload spend per calendar month. null = no cap. Sending 0 is treated as null
has_payment_methodbooleanDerived — true iff a default Stripe payment method is set
has_ever_topped_upbooleanDerived — true once the account has at least one successful manual top-up or auto-reload

Get billing account

cURL
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."}.

Enable auto-reload
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
  }'
Disable auto-reload
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}'
FieldTypeRequired when enabling
auto_reload_enabledboolean
auto_reload_minimum_centsinteger ≥ 0yes — triggers a reload once balance drops below
auto_reload_amount_centsinteger > 0yes — amount charged on reload
auto_reload_monthly_limit_centsinteger ≥ 0, or nullno — 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
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
result = requests.post(
    "https://api.thunderphone.com/v1/billing/top-up",
    headers={"Authorization": "Bearer sk_live_YOUR_API_KEY"},
    json={"amount_cents": 10000},
).json()
FieldTypeRequiredDescription
amount_centsinteger ≥ 1yesUSD cents. No server-enforced maximum; the dashboard suggests common amounts
payment_method_idstringnoStripe PM id. Defaults to the org's default

Returns 201 Created:

{
  "payment_intent_id": "pi_abc123",
  "status":            "succeeded",
  "client_secret":     "pi_abc123_secret_..."
}
FieldTypeDescription
payment_intent_idstringStripe PaymentIntent id
statusstringMirrors Stripe's own PaymentIntent.status — typically succeeded, requires_action, or requires_payment_method
client_secretstringPass 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:

402 example
{
  "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"
}
Statuscoderetryablenext_actionCondition
402payment_authentication_requiredtrueauthenticate_payment3-D Secure / SCA required — complete authentication client-side with the PaymentIntent's client_secret, then retry
402card_declinedfalseupdate_payment_methodThe bank declined the card (all decline reasons are collapsed into this one code)
422billing_request_invalidfalseupdate_payment_methodStripe rejected the request (bad parameters, unknown customer)
503billing_provider_unavailabletrueretry_paymentTransient Stripe outage or rate limit; retry shortly
502billing_provider_errortrueretry_paymentAny 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
curl -X POST https://api.thunderphone.com/v1/billing/setup-intent \
  -H "Authorization: Bearer sk_live_YOUR_API_KEY"
Response
{
  "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
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
curl 'https://api.thunderphone.com/v1/billing/transactions?limit=200' \
  -H "Authorization: Bearer sk_live_YOUR_API_KEY"
Python
txns = requests.get(
    "https://api.thunderphone.com/v1/billing/transactions",
    headers={"Authorization": "Bearer sk_live_YOUR_API_KEY"},
    params={"limit": 200},
).json()

Query parameters

ParamTypeDefaultDescription
limitinteger2001–1000
offsetinteger0
kindstringFilter to one transaction kind
start_dateISO 8601Filter on created_at
end_dateISO 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"
}
FieldTypeDescription
idintegerTransaction id
kindstringOne of manual_topup, auto_reload, call_usage, phone_number_monthly, conversion_report, adjustment
descriptionstringHuman-readable summary
amount_centsintegerNegative for usage/fees, positive for credits
amount_usdstringFormatted signed amount
call_idinteger | nullSet on call_usage rows
phone_number_idinteger | nullSet on phone_number_monthly rows
phone_number_valuestring | nullE.164 for the number billed
metadataobjectKind-specific details
created_attimestamp

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.