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.
We don't have a public developer portal yet. To register an OAuth application, email [email protected] with your app name, redirect URI(s), and whether you need refresh tokens. We'll send back a client_id. Apps are public clients (no client secret) — PKCE is required.
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. Custom schemes (e.g.
myapp://callback) are supported for 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.
- 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/main_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.