OAuth 2.0 for applications

Let customers connect their Phone.inc company to your SaaS without sharing passwords or API keys. This guide covers company-owned, confidential applications: S256 PKCE, resource-specific scopes, and client authentication on your server.

Start with Applications for use cases. For your own company's scripts, use an API key. Public MCP connectors and existing native or legacy clients use a separate OAuth reference; their secretless api flow is not the registration or authorization flow for company-owned applications.

All endpoints below use https://app.phone.inc.

Company-owned SaaS applications

Your publishing company needs the Applications feature flag enabled by Phone.inc; application management is off by default. Customer companies do not need the flag to install or use your application. Disabling the flag blocks application management, not existing OAuth connections or API access.

Phone.inc support can separately suspend an application. Suspension blocks API and MCP access, new authorizations, code exchanges, refreshes, caller-context requests, and queued webhook deliveries. It retains installations and credentials rather than revoking them. Resuming restores access for credentials that remain valid; deliveries skipped during suspension are not replayed automatically. Requests already in flight may finish. Suspension is an internal support control, not a partner API or MCP tool.

Once enabled, sign in with your ordinary Phone.inc company and open Applications, alongside API keys. Create a private application at /applications/new with a name, description, and exact redirect URIs, one per line. Edit it at /applications/:id/edit. No separate partner account or public marketplace is required. Do not register these applications through /oauth/register.

Redirect URIs must use HTTPS, except HTTP loopback callbacks on 127.0.0.1 or [::1]. Register the exact callback, including its port and path; localhost and custom schemes are not supported for these apps. Optionally configure an HTTPS context_endpoint_url without credentials, a query string, or a fragment. API-only applications do not need a context endpoint.

Optionally register connect_url, labeled Connection URL in the dashboard. This HTTPS URL on your SaaS starts OAuth using your own signed-in user session, state, and fresh S256 PKCE. It is not the OAuth callback URL. Customers can use it to update permissions or reconnect a disconnected app. Changing it alone is metadata and does not disconnect installations.

Enable only the resource-specific scopes your app needs. No scopes are preselected. Every authorization request must ask for a nonempty subset of enabled scopes, including their dependencies. Company-owned applications cannot request api, web, api:read, or api:write.

Adding messages:* permissions requires your publishing company's Messaging feature flag, including when creating an application. Disabling that flag does not remove previously enabled permissions or disconnect existing installations. Customers still need their own Messaging flag to use messaging endpoints, and Message attachments to retrieve attachments.

Copy the client ID, client secret, and separate context signing secret when shown on creation or rotation. Secrets are shown only once. Keep them server-side in a secret manager, never in browser code, mobile binaries, URLs, or logs. App credential creation, management, rotation, and deletion are interactive dashboard actions, not API or MCP tools.

Discovery metadata

Fetch https://app.phone.inc/.well-known/oauth-authorization-server for endpoint URLs, supported grants, S256 PKCE, and client authentication methods. Company-owned apps use client_secret_basic or client_secret_post, not none. The server advertises both public and confidential clients; advertised api support does not make that scope available to company-owned apps.

Protected-resource discovery is available at /.well-known/oauth-protected-resource and /.well-known/oauth-protected-resource/mcp. The latter identifies https://app.phone.inc/mcp. See the public-client discovery reference for the shared metadata contract.

Step 1: Build the authorization URL

Generate a fresh random PKCE verifier, its base64url-encoded SHA-256 challenge (without padding), and a random state. Store the verifier and state in your server-side session, bound to the signed-in SaaS user and intended tenant. Redirect the customer's browser to /oauth/authorize with:

ParameterValue
response_typecode
client_idYour application's client ID
redirect_uriAn exact registered callback URI
scopeSpace-separated, nonempty subset of enabled resource scopes, including dependencies
stateFresh random value to validate at the callback
code_challengeBase64url-encoded SHA-256 of the verifier
code_challenge_methodS256

For example, an app enabled for calls:read can send the browser to this URL, replacing the state and challenge with freshly generated values:

https://app.phone.inc/oauth/authorize?response_type=code&client_id=your_client_id&redirect_uri=https%3A%2F%2Fyour-app.com%2Fcallback&scope=calls%3Aread&state=random_csrf_token&code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM&code_challenge_method=S256

Any authenticated employee in the customer company can approve installation. Consent shows the publisher, application, and requested permissions. Requesting caller_context:provide explicitly discloses that caller phone numbers will be sent to your app. No customer company-admin role is required.

For an existing installation, consent distinguishes additional permissions, already approved permissions, and previously approved permissions omitted from this request, which will be removed. Request the full desired scope set, including permissions to keep and their dependencies, not just the additions. Approval updates the same installation; cancelling changes nothing.

Approval redirects to your callback with code and state. Denial returns error=access_denied and state instead. Validate state against the saved session before proceeding, including on error callbacks. Never accept a callback for a different SaaS tenant.

Step 2: Exchange the code for an access token

Exchange the code within 10 minutes at POST /oauth/token. Send form-encoded fields and authenticate the confidential client. PKCE is mandatory even with a client secret. Use one authentication method per request: HTTP Basic (client_secret_basic), or the client_id and client_secret form fields (client_secret_post). The examples below run on your server and use Basic authentication.

Code exchange

POST
/oauth/token
curl https://app.phone.inc/oauth/token \
  --user "$PHONE_INC_CLIENT_ID:$PHONE_INC_CLIENT_SECRET" \
  --data-urlencode grant_type=authorization_code \
  --data-urlencode "code=$AUTHORIZATION_CODE" \
  --data-urlencode redirect_uri=https://your-app.com/callback \
  --data-urlencode "code_verifier=$PKCE_VERIFIER"

Response

{
  "access_token": "access_token_value",
  "token_type": "Bearer",
  "expires_in": 3600,
  "refresh_token": "refresh_token_value",
  "scope": "calls:read",
  "created_at": 1792584000,
  "installation_id": "019daef1-085e-73f0-85f6-a71e2d8dd2cc",
  "company_id": "019daef1-085e-73f0-85f6-a71e2d8dd2cd"
}

Map installation_id to the correct SaaS tenant. It is the installed integration's public UUID; company_id is the customer company's public UUID. Repeated authorization updates one installation per company and app, visible on the customer's Integrations page. Supplying another company_id or installation_id to authorization cannot select another tenant. Tokens remain bound to the approved company and installation.

Step 3: Call the API

Send the access token as Authorization: Bearer <access_token> to the existing /api/v1/* endpoints. Resource-page examples use API keys; replace that credential with the installation's OAuth Bearer token. Only granted actions and data are accessible. For the example calls:read grant:

curl https://app.phone.inc/api/v1/calls \
  -H "Authorization: Bearer $PHONE_INC_ACCESS_TOKEN"

Existing MCP tools can also use partner tokens under the same API permissions. They do not bypass consent or provide app-credential management.

Refreshing tokens

Access tokens last one hour. Refresh tokens are valid for 30 days from issue. Refresh server-side with the current refresh token and confidential client authentication, just as at code exchange:

curl https://app.phone.inc/oauth/token \
  --user "$PHONE_INC_CLIENT_ID:$PHONE_INC_CLIENT_SECRET" \
  --data-urlencode grant_type=refresh_token \
  --data-urlencode "refresh_token=$PHONE_INC_REFRESH_TOKEN"

For client_secret_post, replace --user with form fields client_id and client_secret. Store the latest returned access and refresh tokens and discard the previous refresh token. Refresh responses contain scope, installation_id, and company_id as at code exchange; refresh cannot change the installation or company binding. If refresh fails because access was disconnected or revoked, start a new authorization flow rather than retrying indefinitely.

Existing tokens cannot gain newly enabled or approved scopes, and refresh cannot expand their scope set. To obtain additional permissions, your SaaS must initiate a fresh S256 PKCE authorization requesting the full desired scope set and exchange the resulting code.

Revoking tokens

Send an access or refresh token to POST /oauth/revoke, authenticating the confidential client as at code exchange:

curl https://app.phone.inc/oauth/revoke \
  --user "$PHONE_INC_CLIENT_ID:$PHONE_INC_CLIENT_SECRET" \
  --data-urlencode "token=$PHONE_INC_ACCESS_TOKEN"

For client_secret_post, send client_id and client_secret as form fields instead of Basic authentication. Successful revocation returns 200 OK; unknown tokens also return success per RFC 7009. Missing or invalid client authentication is not a successful revocation.

Revoking one of this application's installation tokens revokes the whole installation, not just that token. All installation access tokens, refresh tokens, and pending authorization codes are revoked. Caller lookups stop, webhook subscriptions are deleted, and queued deliveries and retries are blocked.

An expired access token can still disconnect its installation. Tokens already revoked return success without affecting a later reconnection; use a token from the current connection to disconnect it.

Disconnect, rotate, and reauthorize

Any authenticated employee in the customer company can disconnect the application from Integrations. Dashboard disconnect and application deletion have the same installation-wide effect as revocation.

Adding enabled application scopes does not disconnect installations, revoke existing tokens, stop caller context, or delete webhooks. It makes additional permissions available for fresh consent, not automatically granted. In Integrations, customers see Additional permissions available and an Update permissions link to the configured Connection URL. Without that URL, the dashboard instructs them to open your SaaS. The dashboard does not itself grant access or expand tokens.

Reducing installation permissions through consent, or removing enabled application scopes, revokes only credentials containing removed scopes. Unaffected narrower tokens and installations continue. Removing every installed permission disconnects that installation. Webhook scope ceilings and event lists shrink permanently when permissions are removed; readding a scope cannot restore an old subscription's removed access. Create a new subscription for expanded access.

Changing redirect URIs or the context endpoint still disconnects installations and requires fresh consent. Name, description, and Connection URL changes do not. Rotation replaces both the client secret and context signing secret and disconnects installations. Update your server's secrets and ask customers to reauthorize; this is not the overlapping rotate-then-revoke procedure used for API keys.

Historical call context remains available after disconnection. Reconnecting cannot resurrect old webhook subscriptions or deliveries; create new subscriptions after consent. See webhook permissions.

If you configure caller lookup, implement the signed context request. It uses the separate context signing secret, not the OAuth client secret.

Scopes

Company-owned partner applications use these 23 resource-specific scopes. Every :write scope requires its matching :read scope in both enabled and requested permissions. calls:control also requires calls:read. Other scopes have no dependencies: messages:send is independent and is not implied by messages:write.

ScopePermission
calls:readRead calls, recordings, and transcripts
calls:writeMark calls read or unread
calls:controlTransfer and control active calls
messages:readRead text messages
messages:writeManage text templates and texting settings
messages:sendSend text messages
employees:readRead employees
employees:writeManage employees
business_numbers:readRead business numbers and routing
business_numbers:writeManage business numbers and routing
contacts:readRead contacts
contacts:writeManage contacts
voicemails:readRead voicemails, recordings, and transcripts
voicemails:writeManage and delete voicemails
departments:readRead departments
departments:writeManage departments
company:readRead company details
company:writeUpdate company details
integrations:readRead installed integrations
webhooks:readRead this application's webhooks
webhooks:writeManage this application's webhooks
caller_context:readLook up caller context
caller_context:provideReceive caller phone numbers and provide caller context

API action permissions

Permissions follow explicit API actions, not HTTP methods alone. Unlisted actions are not available to partner OAuth tokens.

API actionsRequired scope
List and retrieve calls; retrieve active callscalls:read
Update calls; mark calls seencalls:write and calls:read
Create or cancel transfers; complete transferscalls:control and calls:read
List and retrieve voicemailsvoicemails:read
Update or delete voicemailsvoicemails:write and voicemails:read
List and retrieve business numbersbusiness_numbers:read
Update business numbers and incoming-call, outside-hours, or no-answer routing; create or remove terminations; create porting requests or voice previewsbusiness_numbers:write and business_numbers:read
Retrieve texting settings, messages, message attachments, or text snippets; list text snippetsmessages:read
Update texting settings; create, update, or delete text snippetsmessages:write and messages:read
Send business-number messages; retry messagesmessages:send
List or retrieve employees, contacts, or departments; retrieve employee do-not-disturb settingsMatching resource :read
Create, update, or delete employees, contacts, or departments; update employee do-not-disturb settingsMatching resource :write and :read
Retrieve or update company detailscompany:read, plus company:write for updates
List or retrieve installed integrationsintegrations:read
Look up caller contextcaller_context:read
List webhooks; list webhook deliverieswebhooks:read
Create, update, or delete webhooks; send a webhook testwebhooks:write and webhooks:read
Test partner caller contextcaller_context:provide

Combined /api/v1/inbox, /api/v1/inbox/:phone/activity, and /api/v1/activity/unseen require at least one of calls:read, voicemails:read, or messages:read. Rows, latest items, previews, timestamps, unseen counts, and pagination include only granted resource types. An explicit sources filter can narrow access, never expand it; selecting no readable source returns 422. Contact metadata and contact-name searches require contacts:read; phone filters and phone-only queries do not.

Marking inbox activity seen requires at least one corresponding :write scope with its dependency :read, and only marks writable sources. Read-only sources remain untouched. The legacy /api/v1/calls/seen endpoint checks call and voicemail write permissions separately: calls:write cannot alter voicemail seen state.

Response fields and outbound delivery

calls:read includes each call's recordings, transcriptions, summaries, and search. voicemails:read includes voicemail recordings and transcriptions. messages:read includes text content and attachment metadata and download links when both Messaging and Message attachments are enabled for the customer. These permissions also apply to embedded content; there is no independent content scope.

Reference IDs remain available, but embedded employee, contact, department, and business-number details require employees:read, contacts:read, departments:read, and business_numbers:read, respectively. This includes business-number names, numbers, and formatted-number fields.

caller_context:provide alone is a valid context-only installation and requires an app context endpoint. Phone.inc sends caller phone numbers only when that installation grants caller_context:provide. caller_context:read permits the lookup API, not outbound phone-number delivery. See signed partner lookups.

Webhook subscriptions belong to the creating installation and retain a granted-scope ceiling that can shrink but never expand. Events require only the event resource's read scope: call transcript and summary events need calls:read; voicemail transcript events need voicemails:read. Payloads redact ungranted embedded data. Subscriptions never silently gain permissions. See webhook permissions.

Integration setup actions (integration_setups) are dashboard-only for partner OAuth and are not granted by integrations:read. Ordinary installed-integration reads remain available. App credential management is also dashboard-only and excluded from MCP; no tool names or counts change.

Legacy api access for older clients and public MCP clients, first-party api/web behavior, and API keys remain compatible. These restrictions do not change API-key permissions. web remains reserved for first-party browser sessions. Use the public and legacy reference when maintaining those clients; do not use its secretless examples for company-owned applications.

Error responses

OAuth endpoints return the standard OAuth error shape:

OAuth error

{
  "error": "invalid_grant",
  "error_description": "The provided authorization grant is invalid, expired, revoked, or does not match the redirection URI."
}
ErrorMeaning
invalid_requestRequired parameter missing, malformed, or repeated
invalid_clientUnknown client or missing/invalid confidential client authentication
invalid_grantCode or refresh token invalid, expired, revoked, or already used
invalid_scopeScope unknown, not enabled, or missing required dependencies
unauthorized_clientClient not authorized for this grant type
access_deniedUser denied consent; returned as a callback parameter

An invalid, expired, or revoked API token returns 401. Refresh an expired token and retry once. If the installation is disconnected, obtain fresh consent. A suspended application receives 401 for API/MCP access and invalid_client on new authorization attempts; code exchanges and refreshes return invalid_grant. Contact support rather than repeatedly retrying. Do not retry permission errors by requesting unrelated data.

Security checklist

  • Use fresh S256 PKCE and state for every authorization; validate state before exchanging the code.
  • Authenticate the client at code exchange, refresh, and revocation. PKCE does not replace client authentication.
  • Keep client secrets, signing secrets, and tokens server-side. The client ID is not a secret.
  • Bind callbacks and installation IDs to the intended SaaS tenant; keep each customer's tokens isolated.
  • Request only enabled resource scopes and their dependencies.
  • Provide a disconnect and reauthorization path; revocation affects the entire installation.