OAuth 2.0 for partners
OAuth lets your application act on behalf of other Phone.inc customers without ever holding their credentials. Use this when you're shipping a product — a Slack bot, a CRM connector, a workflow tool — that hundreds of Phone.inc customers might install.
If you're an end customer building your own internal scripts or server-to-server integrations, you don't need OAuth. Use an API key — it's faster to set up and easier to operate.
We support a single grant type: authorization code with PKCE. It works for native apps, single-page apps, and traditional server-side web apps. The base URL for everything below is https://app.phone.inc.
MCP clients (Claude, ChatGPT, Cursor) can register themselves automatically via dynamic client registration. For a stable client_id you control long-term, email support@phone.inc with your app name and redirect URI(s). Apps are public clients (no client secret) — PKCE with S256 is required and enforced.
Discovery metadata
MCP clients and other OAuth libraries discover our authorization server automatically. Fetch either document from your app's origin:
GET /.well-known/oauth-authorization-server— authorization, token, revoke, and registration endpointsGET /.well-known/oauth-protected-resource— the protected resource identifier and linked authorization serverGET /.well-known/oauth-protected-resource/mcp— the same metadata scoped to the MCP endpoint (resource: https://app.phone.inc/mcp)
Both return JSON. The issuer field matches the host you called (https://app.phone.inc in production). The root protected-resource document uses that host as resource; the MCP sub-path document uses https://app.phone.inc/mcp.
Authorization server metadata
{
"issuer": "https://app.phone.inc",
"authorization_endpoint": "https://app.phone.inc/oauth/authorize",
"token_endpoint": "https://app.phone.inc/oauth/token",
"revocation_endpoint": "https://app.phone.inc/oauth/revoke",
"registration_endpoint": "https://app.phone.inc/oauth/register",
"response_types_supported": ["code"],
"grant_types_supported": ["authorization_code", "refresh_token"],
"code_challenge_methods_supported": ["S256"],
"token_endpoint_auth_methods_supported": ["none"],
"scopes_supported": ["api"]
}
Register an OAuth client
Native apps installed side by side should use distinct callback schemes and
register each exact redirect URI with their client. Phone.inc Android previews
use inc.phone.preview.pr<PR>://oauth/callback, accepted only for our first-party
public mobile client; standard mobile builds keep phoneinc://oauth/callback. These custom schemes
are not accepted by dynamic client registration. Token exchange must use the
same redirect URI as the authorization request.
Android preview token exchanges require an exact string match, including
rejecting added query parameters.
POST /oauth/register implements RFC 7591 dynamic client registration. MCP connectors call this before starting the authorization flow. Send application/json:
Registration request
curl -X POST https://app.phone.inc/oauth/register \
-H "Content-Type: application/json" \
-d '{
"client_name": "My MCP connector",
"redirect_uris": ["http://127.0.0.1:8765/callback"],
"token_endpoint_auth_method": "none"
}'
Response
{
"client_id": "a1b2c3d4e5f6g7h8i9j0",
"client_id_issued_at": 1730895600,
"redirect_uris": ["http://127.0.0.1:8765/callback"],
"token_endpoint_auth_method": "none",
"grant_types": ["authorization_code", "refresh_token"],
"response_types": ["code"],
"client_name": "My MCP connector",
"scope": "api"
}
Registered clients are always public (token_endpoint_auth_method: none), always scoped to api, and always show the consent screen. There is no client_secret.
Allowed redirect URIs
- Name
https://- Type
- allowed
- Description
Production callbacks on your own domain.
- Name
http://127.0.0.1, http://localhost, http://[::1]- Type
- allowed
- Description
Loopback HTTP with any port — for Claude Desktop, Cursor, and other local MCP clients.
- Name
Custom schemes (myapp://)- Type
- not via DCR
- Description
Native app deep links are only available for manually registered first-party clients.
Overview
The flow has three steps:
- Authorize — redirect the user to
/oauth/authorize. They sign in to Phone.inc and approve your app. - Exchange — your app receives an authorization
codeon the redirect URI and exchanges it for anaccess_tokenat/oauth/token. - Call the API — send the access token as
Authorization: Bearer <token>on every API request.
Access tokens are scoped to the employee who authorized your app. They expire after 1 hour, and you can use the long-lived refresh_token to mint new ones for up to 30 days without prompting the user again.
Step 1: Build the authorization URL
Generate a PKCE code verifier and challenge, then redirect the user's browser to:
Authorization request
# This is a browser redirect, not a direct API call. Open in the user's browser:
https://app.phone.inc/oauth/authorize\
?response_type=code\
&client_id=your_client_id\
&redirect_uri=https%3A%2F%2Fyour-app.com%2Fcallback\
&scope=api\
&state=random_csrf_token\
&code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM\
&code_challenge_method=S256
Required parameters
- Name
response_type- Type
- string
- Description
Must be
code.
- Name
client_id- Type
- string
- Description
The client ID issued when you registered your app.
- Name
redirect_uri- Type
- string
- Description
Where to send the user after they approve your app. Must match a redirect URI registered with your app exactly. HTTPS callbacks and loopback HTTP (
http://127.0.0.1,http://localhost,http://[::1]with any port) are supported for web and MCP clients. Custom schemes (e.g.myapp://callback) are only available for manually registered native apps.
- Name
scope- Type
- string
- Description
Space-separated list of scopes. Use
apifor full access on behalf of the authenticated employee.
- Name
code_challenge- Type
- string
- Description
The PKCE challenge — base64url-encoded SHA-256 of the code verifier. Required for all public clients; requests without it are rejected.
- Name
code_challenge_method- Type
- string
- Description
Must be
S256.
- Name
state- Type
- string
- Description
Opaque value that is echoed back on the redirect. Use it to prevent CSRF — generate it per request and validate it on the callback.
After the user signs in and approves the app, the browser is redirected to:
https://your-app.com/callback?code=AbCdEf123456&state=random_csrf_token
If the user denies the request, you'll get ?error=access_denied&state=... instead.
Step 2: Exchange the code for an access token
Within 10 minutes of receiving the authorization code, exchange it for tokens by POSTing to /oauth/token. Send the request body as application/x-www-form-urlencoded.
Token request
curl -X POST https://app.phone.inc/oauth/token \
-d grant_type=authorization_code \
-d code=AbCdEf123456 \
-d redirect_uri=https://your-app.com/callback \
-d client_id=your_client_id \
-d code_verifier=the_original_pkce_verifier
Response
{
"access_token": "Pj7vXq2L0aN4mYtRk8sGdH9eW6cZbF1uIoP3xKjQ",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "Rt8sFq1MaZ3oV9pKjXhB4nW2yL7eC5uIdN6gHbPwE",
"scope": "api",
"created_at": 1730895600
}
Store both tokens securely. Treat the refresh token like a password — never expose it to the browser if you can avoid it.
Step 3: Call the API
Pass the access token in the Authorization header on every API request. The same /api/v1/* endpoints that accept API keys also accept OAuth bearer tokens — Phone.inc auto-detects which is which.
Authenticated request
curl https://app.phone.inc/api/v1/business_numbers \
-H "Authorization: Bearer Pj7vXq2L0aN4mYtRk8sGdH9eW6cZbF1uIoP3xKjQ"
If the token is missing, malformed, expired, or revoked, you'll get a 401 Unauthorized with a JSON body describing the problem. Time to refresh.
Refreshing tokens
Access tokens last 1 hour. To get a new one without prompting the user, exchange your refresh token at the same /oauth/token endpoint with grant_type=refresh_token. Refresh tokens are valid for 30 days from issue, and we issue a new refresh token on every refresh — store the latest one and discard the previous.
Refresh request
curl -X POST https://app.phone.inc/oauth/token \
-d grant_type=refresh_token \
-d refresh_token=Rt8sFq1MaZ3oV9pKjXhB4nW2yL7eC5uIdN6gHbPwE \
-d client_id=your_client_id
Response
{
"access_token": "QmJ0wYr3Mb...",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "St9tGr2Nb...",
"scope": "api",
"created_at": 1730899200
}
Revoking tokens
Call /oauth/revoke to invalidate an access or refresh token immediately — for sign-out, "log out everywhere", or after a suspected leak. The endpoint always returns 200 OK (even for unknown tokens) per RFC 7009.
Revoke request
curl -X POST https://app.phone.inc/oauth/revoke \
-d token=Pj7vXq2L0aN4mYtRk8sGdH9eW6cZbF1uIoP3xKjQ \
-d client_id=your_client_id
Scopes
- Name
api- Type
- default
- Description
Full access to
/api/v1/*on behalf of the authenticated employee. This is the only scope third-party apps should request.
- Name
web- Type
- reserved
- Description
Reserved for first-party browser sessions. Don't request it from a third-party app — it won't be granted.
Error responses
The OAuth endpoints return errors in the standard OAuth 2.0 shape:
OAuth error
{
"error": "invalid_grant",
"error_description": "The provided authorization grant is invalid, expired, revoked, or does not match the redirection URI."
}
Common error codes:
- Name
invalid_request- Type
- 400
- Description
Required parameter missing, malformed, or repeated.
- Name
invalid_client- Type
- 401
- Description
The
client_idis unknown.
- Name
invalid_grant- Type
- 400
- Description
The authorization code or refresh token is invalid, expired, or already used.
- Name
invalid_scope- Type
- 400
- Description
The requested scope is unknown or not allowed for your app.
- Name
unauthorized_client- Type
- 400
- Description
Your app isn't authorized for this grant type.
- Name
access_denied- Type
- 302
- Description
The user denied your authorization request. Returned as a redirect parameter, not a JSON body.
When you call /api/v1/* with a bad token, you'll get a 401 with WWW-Authenticate: Bearer error="invalid_token". Treat it as a signal to refresh and retry once.
Security checklist
- Use a fresh PKCE verifier and
statevalue for every authorization request. - Validate
stateon the callback before exchanging the code. - Never embed your client ID in a public web app and assume it's secret — it isn't. Security comes from PKCE plus the registered redirect URIs.
- Store refresh tokens server-side or in secure platform storage (Keychain on iOS, EncryptedSharedPreferences on Android). Never put them in
localStorage. - Revoke tokens on sign-out.