Public and legacy OAuth clients

This reference covers public MCP connectors and existing public native and legacy OAuth clients. They use authorization code flow with S256 PKCE and no embedded client secret.

Building a SaaS that customer companies install? Use Applications and the confidential company-app guide instead. Dashboard-created applications require resource-specific scopes and server-side client authentication; the secretless api examples here do not apply to them.

The existing public-client api scope and first-party api/web behavior remain compatible. web is reserved for first-party browser sessions, not dynamically registered clients. API keys keep their existing permissions. See MCP for connector setup and available tools.

Discovery metadata

Fetch these JSON documents from the Phone.inc origin (https://app.phone.inc in production), not your own application's origin:

  • GET /.well-known/oauth-authorization-server — issuer, authorization, token, revocation, and registration endpoints; supported response types, grants, PKCE methods, client authentication methods, and scopes.
  • GET /.well-known/oauth-protected-resource — the root resource identifier, linked authorization server, Bearer methods, and scopes.
  • GET /.well-known/oauth-protected-resource/mcp — the same protected-resource metadata for resource: https://app.phone.inc/mcp.

The issuer matches the host called. The root protected resource uses that host as resource; the MCP document includes /mcp. Authorization-server metadata advertises response_types_supported: ["code"], grant_types_supported: ["authorization_code", "refresh_token"], and code_challenge_methods_supported: ["S256"]. Client authentication methods are none, client_secret_basic, and client_secret_post. Protected-resource documents advertise bearer_methods_supported: ["header"].

The shared scopes_supported list contains api and the 23 company-app scopes. It describes the server, not permissions available to every client: dynamic registration issues public clients scoped to api, while company-owned apps use their enabled resource scopes.

Register an OAuth client

POST /oauth/register implements RFC 7591 dynamic registration for public MCP connectors. Send JSON:

Registration

POST
/oauth/register
curl 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"
}

Dynamically registered clients are always public, scoped to api, and show consent. They have no client_secret. Dynamic registration does not create a company-owned SaaS application.

Register exact redirect URIs. HTTPS callbacks and loopback HTTP on 127.0.0.1, localhost, or [::1] with any port are supported. Custom schemes are not accepted by dynamic registration; they are only available to manually registered first-party native clients. These rules differ from company-owned application callbacks, which do not accept localhost.

Authorize with PKCE

Generate a fresh random verifier and state for each request. The challenge is base64url-encoded SHA-256 of the verifier, without padding. Keep the verifier and state securely until the callback. Open this authorization URL in the user's browser, substituting your registered client ID, exact callback, and freshly generated state and challenge:

https://app.phone.inc/oauth/authorize?response_type=code&client_id=your_client_id&redirect_uri=http%3A%2F%2F127.0.0.1%3A8765%2Fcallback&scope=api&state=random_csrf_token&code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM&code_challenge_method=S256

All public clients require PKCE with code_challenge_method=S256. The user signs in and approves access on behalf of the authenticated employee. The callback includes code and state, or error=access_denied and state on denial. Validate state before exchanging the code.

Exchange the code

Within 10 minutes, send form-encoded fields to /oauth/token. The redirect URI must match the authorization request exactly. Public clients send their client ID and verifier without a client secret:

curl https://app.phone.inc/oauth/token \
  --data-urlencode grant_type=authorization_code \
  --data-urlencode "client_id=$PHONE_INC_CLIENT_ID" \
  --data-urlencode "code=$AUTHORIZATION_CODE" \
  --data-urlencode redirect_uri=http://127.0.0.1:8765/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": "api",
  "created_at": 1730895600
}

Use and refresh tokens

Send Authorization: Bearer <access_token> to /api/v1/* or the MCP endpoint. Tokens act on behalf of the authorizing employee. Access tokens last one hour; refresh tokens are valid for 30 days from issue. Store tokens in secure platform storage, never in source code or browser localStorage.

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

The response has the same token fields as code exchange. Store the newly issued refresh token and discard the previous one. An invalid, expired, or revoked API token returns 401; refresh and retry once, or restart authorization if refresh fails. OAuth endpoint errors use error and error_description; see OAuth errors.

Revoke a token

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

Send an access or refresh token. Successful revocation returns 200 OK, including for unknown tokens, per RFC 7009. Do not apply the confidential company-app installation lifecycle to public clients; company-app revocation requires client authentication and disconnects the whole installation.

First-party native compatibility

Native builds installed side by side should use distinct callback schemes and register each exact redirect URI with their client. Standard Phone.inc mobile builds keep phoneinc://oauth/callback. Android previews use inc.phone.preview.pr<PR>://oauth/callback, accepted only for our first-party public mobile client. These custom schemes are not accepted by dynamic registration or company-owned applications.

Token exchange must use the same redirect URI as authorization. Android preview exchanges require an exact string match, including rejecting added query parameters. First-party clients retain their existing consent behavior; dynamic registration cannot opt a client into first-party status.