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.

Overview

The flow has three steps:

  1. Authorize — redirect the user to /oauth/authorize. They sign in to Phone.inc and approve your app.
  2. Exchange — your app receives an authorization code on the redirect URI and exchanges it for an access_token at /oauth/token.
  3. 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

GET
/oauth/authorize
# 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 api for 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

POST
/oauth/token
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

GET
/api/v1/main_numbers
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

POST
/oauth/token
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

POST
/oauth/revoke
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_id is 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 state value for every authorization request.
  • Validate state on 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.