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

Overview

Authentication

How to authenticate with the ThunderPhone API using sk_live_ bearer keys, pick the organization your requests act on, and read the shared error format.

The ThunderPhone API is a REST + JSON API. Every public endpoint accepts an Organization API key via the standard OAuth 2.0 Bearer scheme and returns JSON with UTF-8 encoding.

Base URL

https://api.thunderphone.com/v1

All examples in this reference are rooted at that base URL. The API is versioned via the /v1 path prefix — we add new fields additively and avoid breaking changes within v1.

API keys

Create a key from the dashboard:

  1. Sign in
  2. Open Keys

    Navigate to Settings → Keys.

  3. Create key

    Click Create API Key, give it a name (e.g. production, ci), and copy the sk_live_... value. The raw key is shown only once — if you lose it, revoke and create a new one.

Making a request

Send the key as a bearer token in the Authorization header. The API infers your organization from the key, so you don't need an org id in the URL path.

cURL
curl https://api.thunderphone.com/v1/agents \
  -H "Authorization: Bearer sk_live_YOUR_API_KEY"
Python
import requests
 
resp = requests.get(
    "https://api.thunderphone.com/v1/agents",
    headers={"Authorization": "Bearer sk_live_YOUR_API_KEY"},
    timeout=10,
)
resp.raise_for_status()
agents = resp.json()
Node.js
const response = await fetch(
  "https://api.thunderphone.com/v1/agents",
  { headers: { Authorization: `Bearer ${process.env.THUNDERPHONE_API_KEY}` } },
);
if (!response.ok) throw new Error(await response.text());
const agents = await response.json();

Health check

Verify API connectivity without authentication:

curl https://api.thunderphone.com/v1/healthz
Response
{ "ok": true }

Dashboard access (X-ThunderPhone-Org)

The dashboard at app.thunderphone.com authenticates users with a personal session/token (one user can belong to many organizations). Those requests identify the target organization via the X-ThunderPhone-Org header:

Authorization: Token <personal-token>
X-ThunderPhone-Org: 42

You will normally not need this header — it exists for the dashboard code path. API integrations built against an sk_live_ key should omit it; the key already carries an org binding and the header, if present, is ignored for API-key requests.

ID format

Most core resources use 64-bit integer ids (agents, phone numbers, calls, members, organizations, publishable keys, VoIP connections). Newer resources use UUID ids — integrations, developer API keys, webhook endpoints, campaigns, knowledge bases and documents, MCP servers, provider connections, investigations, and test suites — and issue reports are addressed by a UUID trace_id. Each page states the id type in its object table. Tokens (invites, API keys) are opaque strings. Every successful write returns the persisted object including its assigned id:

{
  "id": 12,
  "name": "Customer Support Agent",
  "created_at": "2026-04-20T18:24:10.113Z",
  "updated_at": "2026-04-20T18:24:10.113Z"
}

Timestamps are ISO 8601 with millisecond precision in UTC.

Pagination

List endpoints return a plain JSON array by default. Endpoints that can produce very large result sets (currently calls and issue reports) accept limit and offset query parameters and return a wrapped envelope:

{
  "results": [ /* ... */ ],
  "total":   1234,
  "limit":   50,
  "offset":  0
}

To page forward, add limit to the current offset on each request and stop when offset + limit >= total. See each endpoint's page for the default and maximum limit it accepts.

Errors

The API uses conventional HTTP status codes:

StatusMeaning
200OK
201Created
204No Content — operation succeeded with no body
400Bad Request — invalid parameters. Body contains detail or field-specific messages
401Unauthorized — missing or invalid API key
402Payment Required — insufficient balance
403Forbidden — valid credentials but the resource is out of scope or policy denies the action
404Not Found — resource doesn't exist (or isn't visible to this key)
409Conflict — optimistic-concurrency failures, or an action that requires a live/other state
410Gone — deprecated URL shape (see new_path in body), or an expired/used invite token
422Unprocessable Entity — the request is well-formed but can't be processed (e.g. billing prerequisites not met)
429Too Many Requests — a scoped limit on a specific endpoint (noted on that endpoint's page)
500Internal Server Error
502Bad Gateway — an upstream provider (LiveKit, VoIP, model backend) failed
503Service Unavailable — an upstream provider (e.g. the billing provider) is temporarily unreachable; retry shortly

Error responses include a JSON body with a human-readable detail message:

Error body
{
  "detail": "Call not found."
}

Field-specific validation errors (400) instead map each offending field to a list of messages, using DRF's default format:

Validation error
{
  "name": ["This field is required."],
  "voice": ["Unsupported voice option."]
}

Rate limiting

There is no global rate limit on the API today, and general endpoints never return 429. A small number of specific endpoints carry scoped limits — for example, invite access requests are limited to 3 per hour per user, and simulations enforce a cap on concurrently running calls. These are documented on the relevant endpoint pages and return 429 Too Many Requests when exceeded (no Retry-After header is sent). Treat write operations as non-idempotent unless the endpoint documents otherwise.

Migration from /v1/orgs/<id>/... paths

The API previously nested every resource under your organization id in the URL path — e.g. /v1/orgs/42/agents. Those URLs now return 410 Gone with a body pointing to the new flat shape:

{
  "detail": "This endpoint moved. Drop the /orgs/<id>/ prefix...",
  "legacy_path": "/v1/orgs/42/agents",
  "new_path": "/v1/agents"
}

Existing integrations only need a one-line change: remove /orgs/<id> from every URL. Your sk_live_ key is unchanged and continues to identify your organization automatically.

Next steps