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

Connect tools & data

Connect with OAuth

Authorize MCP clients and the ThunderPhone CLI without sharing an API key.

OAuth lets an app connect to one ThunderPhone organization with the permissions you approve. Directory clients should use OAuth by default. Organization API keys remain available for clients that require a manually configured Bearer token.

Approve a connection

Start the connection in your MCP client. Sign in to ThunderPhone, check the app name and requested permissions, choose an organization, and select Approve. Select Deny if you did not start the connection or do not trust the app. App names are supplied by their developers and are not a verification badge.

Read permissions expose the named data, including recordings and transcripts when calls:read is requested. Write permissions can change or delete resources; calls, campaigns, phone purchases, and deployments can incur costs or affect production. Your organization role still applies.

For a CLI connection, open the verification link shown in your terminal, compare the eight-character code, select your organization, and approve. Merely opening the link does not grant access. Codes expire after 15 minutes.

Disconnect an app

Open Organization → API keys → Authorized apps in the dashboard and choose Revoke. This revokes your selected authorization for the current organization, including its access and refresh tokens. Reconnect from the app if you want to authorize it again.

Discovery for MCP clients

Use the server URL https://api.thunderphone.com/v1/mcp. A request without valid authentication receives 401 with:

WWW-Authenticate: Bearer resource_metadata="https://api.thunderphone.com/.well-known/oauth-protected-resource"

Fetch that document, then fetch the authorization server's metadata at https://api.thunderphone.com/.well-known/oauth-authorization-server. Resource metadata is also available at /.well-known/oauth-protected-resource/v1/mcp. Use the returned endpoints instead of constructing them. The resource identifier is https://api.thunderphone.com/v1/mcp.

The server supports authorization code with S256 PKCE, rotating refresh tokens, public dynamic client registration, revocation, and the device authorization grant. There are no client secrets or implicit grants. OpenID discovery is also available at /.well-known/openid-configuration; it includes the same authorization-server fields plus subject_types_supported: ["public"] and the userinfo endpoint. ID tokens and Client ID Metadata Documents (CIMD) are not supported.

Register a public client

Send JSON to POST /v1/oauth/register:

{
  "client_name": "My MCP client",
  "redirect_uris": ["http://127.0.0.1:8765/callback"],
  "token_endpoint_auth_method": "none",
  "grant_types": ["authorization_code", "refresh_token"],
  "response_types": ["code"]
}

Save the returned client_id. Redirects must use HTTPS, or HTTP on 127.0.0.1 or localhost. Register the exact callback URI, including its port and path. Fragments and embedded credentials are rejected. Optional client_uri and logo_uri must use HTTPS; ThunderPhone does not fetch them during registration. Registration is rate-limited. Registered clients do not expire and remain registered when a connection is revoked. HTTPS callbacks include https://chatgpt.com/connector/oauth/<id> and https://chatgpt.com/connector_platform_oauth_redirect. Each connection may register its own client.

Authorization code

Open the discovered authorization endpoint with client_id, the exact registered redirect_uri, response_type=code, a random state, scope, code_challenge, code_challenge_method=S256, and resource=https://api.thunderphone.com/v1/mcp. Compute the challenge as the unpadded base64url SHA-256 digest of a fresh high-entropy PKCE verifier. Check the returned state and iss before exchanging the code. Every authorization response, including denial and protocol errors, identifies the issuer with iss, exactly matching the discovered issuer. Errors for an invalid client or callback are returned locally without redirecting to that callback.

Exchange at POST /v1/oauth/token using form encoding (JSON is also accepted):

grant_type=authorization_code
client_id=<your client id>
code=<single-use authorization code>
redirect_uri=<exact registered redirect URI>
code_verifier=<original PKCE verifier>
resource=https://api.thunderphone.com/v1/mcp

The optional resource parameter is accepted on both authorization and token requests (including refresh and device exchanges). When omitted, it defaults to the discovered MCP resource. When present, it must exactly match that resource; other values return invalid_target. Access tokens carry that audience, and MCP rejects tokens with a missing or different audience with 401 and a discovery challenge.

Authorization requests and codes expire after 10 minutes. Every token response contains access_token, token_type (Bearer), expires_in (3600 seconds by default), refresh_token, scope, organization_id, and organization_name. Send access tokens only through the Authorization: Bearer header. Never put tokens in URLs, logs, source control, or chat.

Refresh and revoke

Refresh with grant_type=refresh_token, client_id, refresh_token, and resource at the token endpoint. Save the new refresh token atomically and stop using the old one. Refresh tokens expire after 30 days without a successful refresh. An optional scope may narrow the granted permissions. offline_access is always included and refresh tokens are always issued. An initial request without scope grants only offline_access, so clients should request the permissions they need.

Code reuse and rotated refresh-token reuse revoke the entire authorization. Serialize refreshes within a client; replaying a successfully exchanged credential is not a safe retry strategy.

To disconnect, send token and client_id to POST /v1/oauth/revoke. Revoking either token revokes its authorization, including all tokens issued from it. An unknown token returns success without disclosing whether it exists. Dashboard sessions can list their own authorizations at GET /v1/oauth/grants and revoke one at DELETE /v1/oauth/grants/<id>, with X-ThunderPhone-Org selecting the organization.

Device authorization

Device authorization is restricted to pre-registered clients; dynamically registered clients receive unauthorized_client. The pre-registered public client thunderphone-cli supports device authorization and refresh. Send client_id=thunderphone-cli and scope to POST /v1/oauth/device/code. Display user_code and verification_uri to the user, or open verification_uri_complete.

Poll the token endpoint with grant_type=urn:ietf:params:oauth:grant-type:device_code, client_id, and device_code, waiting at least the returned interval (5 seconds). Continue on authorization_pending. On slow_down, use the new interval returned in the response (increased by 5 seconds) for every subsequent request. Stop on access_denied, expired_token, or any other error. Never approve a device code automatically.

The pre-registered thunderphone-mcp client accepts http://127.0.0.1/callback and http://localhost/callback on any port. Scheme, host, path and query must match; the token exchange must use the exact redirect URI (including port) from authorization. Dynamically registered clients require exact redirect-URI matching, including the port. Register a different callback dynamically if you need another path.

Account identity and workspace domain checks

Request both openid email alongside the operation scopes your client needs. Call the discovered userinfo_endpoint (GET /v1/oauth/userinfo) with the access token in the Bearer header. A successful response contains:

{
  "sub": "123",
  "email": "person@example.com",
  "email_verified": true,
  "name": "Example User",
  "org_id": 456
}

sub is the stable user identifier; org_id is the organization selected during consent. The endpoint requires both identity scopes and returns 403 if either is missing. It returns 403 with error=access_denied if the account has no verified email profile, rather than asserting that an unverified email is trusted. Invalid, expired, revoked, or wrong-audience tokens return 401. Human session tokens and organization API keys cannot call userinfo. No ID token is issued.

Available permissions

FamilyScopes
Agents and agent importsagents:read, agents:write
Callscalls:read, calls:write
Phone numbers and VoIPnumbers:read, numbers:write
Knowledgeknowledge:read, knowledge:write
Campaignscampaigns:read, campaigns:write
Integrations, webhook endpoints, MCP serversintegrations:read, integrations:write
Voicesvoices:read
Test scenarios, test runs, validationtesting:read, testing:write
Billingbilling:read
Account identity and verified emailopenid, email (both required for userinfo)
Persistent connectionoffline_access (always included)

GET, HEAD, and OPTIONS use read scopes; other methods use write scopes. Voice and billing mutations are unavailable through OAuth; POST /v1/voices/preview uses voices:read because it previews a voice without changing its configuration. MCP tools enforce the scope of their underlying REST operation. Cross-organization transfers are denied even when the authorizing user belongs to both organizations. Other API families, including API-key management and human account settings, are unavailable through OAuth. A missing permission on a REST operation returns 403 with WWW-Authenticate: Bearer error="insufficient_scope", scope="...". Invalid or expired access tokens return 401.

MCP tool authentication signals

Every tool in tools/list includes securitySchemes: [{"type": "oauth2", "scopes": ["agents:read"]}], with the scope of that tool's REST operation. Public documentation tools use an empty scope list and still require an authenticated connection.

A valid token missing a tool's scope receives HTTP 200 with a JSON-RPC result containing isError: true, explanatory text in content, and _meta["mcp/www_authenticate"]. The latter is an array containing a Bearer challenge with resource_metadata, error="insufficient_scope", error_description, and the required scope. The tool is not executed. Use this challenge to request expanded consent. Missing or invalid authentication continues to return HTTP 401 with WWW-Authenticate; REST scope failures continue to return HTTP 403.

Credential retention

The API runs python manage.py oauth_cleanup hourly in staging and production. It removes expired authorization requests and device codes. Expired access tokens, authorization codes and refresh-token hashes are pruned only after their grant is revoked or the entire family is dead. Spent hashes are retained while the family has a usable refresh token, authorization code, approved device code or access token, so cleanup cannot disable replay detection.