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:
- Sign in
Log into the ThunderPhone Dashboard.
- Open Keys
Navigate to Settings → Keys.
- Create key
Click Create API Key, give it a name (e.g.
production,ci), and copy thesk_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 https://api.thunderphone.com/v1/agents \
-H "Authorization: Bearer sk_live_YOUR_API_KEY"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()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{ "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: 42You 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:
| Status | Meaning |
|---|---|
200 | OK |
201 | Created |
204 | No Content — operation succeeded with no body |
400 | Bad Request — invalid parameters. Body contains detail or field-specific messages |
401 | Unauthorized — missing or invalid API key |
402 | Payment Required — insufficient balance |
403 | Forbidden — valid credentials but the resource is out of scope or policy denies the action |
404 | Not Found — resource doesn't exist (or isn't visible to this key) |
409 | Conflict — optimistic-concurrency failures, or an action that requires a live/other state |
410 | Gone — deprecated URL shape (see new_path in body), or an expired/used invite token |
422 | Unprocessable Entity — the request is well-formed but can't be processed (e.g. billing prerequisites not met) |
429 | Too Many Requests — a scoped limit on a specific endpoint (noted on that endpoint's page) |
500 | Internal Server Error |
502 | Bad Gateway — an upstream provider (LiveKit, VoIP, model backend) failed |
503 | Service 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:
{
"detail": "Call not found."
}Field-specific validation errors (400) instead map each offending
field to a list of messages, using DRF's default format:
{
"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.