Request access OAuth client registration is handled by the OnePageCRM team — request access to get credentials. Learn more

OAuth

OAuth Reference

Every endpoint, parameter, token type, scope, and error code for the OnePageCRM OAuth 2.1 server.

Last updated Jul 14, 2026

Base URL

URL
Authorization server (authorization & tokens)https://secure.onepagecrm.com
CRM APIhttps://app.onepagecrm.com

All OAuth endpoints are on secure.onepagecrm.com. The aud field in every token response contains the CRM API base URL for that user’s account.


Discovery

GET /.well-known/oauth-authorization-server

Returns server capabilities: endpoint URLs, supported scopes, and grant types. No authentication required. Open CORS, safe to call from a browser.

curl https://secure.onepagecrm.com/.well-known/oauth-authorization-server

Response:

{
  "issuer": "https://secure.onepagecrm.com",
  "registration_endpoint": "https://secure.onepagecrm.com/oauth/register",
  "authorization_endpoint": "https://secure.onepagecrm.com/oauth/authorize",
  "token_endpoint": "https://secure.onepagecrm.com/oauth/token",
  "response_types_supported": ["code"],
  "grant_types_supported": ["authorization_code", "refresh_token"],
  "token_endpoint_auth_methods_supported": ["client_secret_basic", "client_secret_post", "none"],
  "code_challenge_methods_supported": ["S256"],
  "scopes_supported": ["crm.readonly", "crm", "mcp"]
}

Although the metadata advertises a registration_endpoint, self-service registration isn’t open during the closed beta; clients are approved by the OnePageCRM team — see Client registration.


Client registration

Client registration is handled by the OnePageCRM team while OAuth is in closed beta — see Client registration for how to get credentials and the redirect URI rules.


Authorization

GET /oauth/authorize

Redirects the user to the OnePageCRM login and consent screen.

https://secure.onepagecrm.com/oauth/authorize
  ?response_type=code
  &client_id=YOUR_CLIENT_ID
  &redirect_uri=https://myapp.example.com/callback
  &scope=crm.readonly
  &state=RANDOM_VALUE
  &code_challenge=BASE64URL_SHA256_OF_VERIFIER
  &code_challenge_method=S256

Parameters:

ParameterRequiredDescription
response_typeYesAlways code
client_idYesYour registered client ID
redirect_uriConditionalRequired if you registered more than one
scopeNoSpace-separated. Defaults to client’s registered scope
stateRecommendedRandom value; verify it in the callback to prevent CSRF
code_challengeYesBASE64URL(SHA256(code_verifier))
code_challenge_methodYesMust be S256

Success response: redirect to your redirect_uri:

https://myapp.example.com/callback?code=AUTH_CODE&state=YOUR_STATE&iss=https://secure.onepagecrm.com

Error response: redirect with error parameters:

https://myapp.example.com/callback?error=access_denied&error_description=...&state=YOUR_STATE
errorWhen it happens
access_deniedUser clicked Decline on the consent screen
invalid_requestMissing or invalid parameter: no PKCE, bad redirect URI
invalid_clientclient_id is not registered
unauthorized_clientClient not allowed this grant type
invalid_scopeScope not registered for this client
server_errorInternal server error

Authorization code grant

POST /oauth/token
Content-Type: application/x-www-form-urlencoded

Exchanges an authorization code for an access token and refresh token. The code is single-use and expires in 10 minutes.

curl -X POST https://secure.onepagecrm.com/oauth/token \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=authorization_code" \
  -d "code=AUTH_CODE" \
  -d "redirect_uri=https://myapp.example.com/callback" \
  -d "client_id=YOUR_CLIENT_ID" \
  -d "code_verifier=YOUR_CODE_VERIFIER"

For confidential clients, authenticate with HTTP Basic instead of client_id in the body:

curl -X POST https://secure.onepagecrm.com/oauth/token \
  -u "YOUR_CLIENT_ID:YOUR_CLIENT_SECRET" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=authorization_code" \
  -d "code=AUTH_CODE" \
  -d "redirect_uri=https://myapp.example.com/callback" \
  -d "code_verifier=YOUR_CODE_VERIFIER"

Parameters:

ParameterDescription
grant_typeauthorization_code
codeThe authorization code from the callback
redirect_uriMust match the one used in the authorization request
client_idYour client ID (public clients only; omit if using HTTP Basic)
code_verifierThe original random string generated before login

Response:

{
  "access_token": "k2tV5VE1b3...",
  "token_type": "Bearer",
  "expires_in": 3600,
  "scope": "crm.readonly",
  "aud": "https://app.onepagecrm.com",
  "refresh_token": "dGhpcyBpcyBh..."
}
FieldDescription
access_tokenUse in Authorization: Bearer header. Valid for 60 minutes
token_typeAlways Bearer
expires_inSeconds until the access token expires
scopeThe granted scope; may be narrower than requested
audThe CRM API base URL for this user’s account
refresh_tokenUse to get a new access token silently

Errors:

errorHTTPWhen it happens
invalid_grant400Code expired, already used, or code_verifier doesn’t match
invalid_client401Wrong client_id or client_secret
invalid_request400Missing parameter, wrong content type, or mixed auth methods
unauthorized_client400Client not allowed this grant type

Refresh token grant

POST /oauth/token
Content-Type: application/x-www-form-urlencoded

Gets a new access token using a refresh token. No user interaction required.

curl -X POST https://secure.onepagecrm.com/oauth/token \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=refresh_token" \
  -d "refresh_token=YOUR_REFRESH_TOKEN" \
  -d "client_id=YOUR_CLIENT_ID"

Parameters:

ParameterRequiredDescription
grant_typeYesrefresh_token
refresh_tokenYesYour current refresh token
client_idYesYour client ID
scopeNoCan request the same or a narrower scope

Response: Same format as the authorization code response. For public clients, a new refresh token is always returned. Replace the old one immediately.

Errors:

errorHTTPWhen it happens
invalid_grant400Refresh token expired, revoked, or already rotated
invalid_client401Wrong client_id
invalid_request400Missing parameter

Client authentication

MethodHowWhen to use
noneclient_id in the POST body, no secretPublic clients (browser/mobile)
client_secret_basicAuthorization: Basic base64(id:secret) headerConfidential clients (recommended)
client_secret_postclient_id + client_secret in the POST bodyConfidential clients (alternative)

Only one method per request. Sending both a Basic header and body credentials is rejected with invalid_request.


Scopes

ScopeAccess
crm.readonlyRead-only access to your CRM data
crmFull read and write access
mcpAccess for AI / MCP integrations

Scopes are layered. The final token scope is the most restrictive of: server-supported → client-registered → user-approved → app-requested. You can always request less than your registered scope, never more.


Token lifetimes

TokenLifetime
Authorization code10 minutes, single-use
Access token60 minutes
Refresh token (public client)7-day idle TTL, 90-day absolute max
Refresh token (confidential client)30-day idle TTL, 365-day absolute max

Idle TTL resets on every use. Absolute max never resets. After 90 days (public) or 365 days (confidential), the user must log in again.

Lifetimes are policy, not physics — they can change. Drive your refresh logic off expires_in from the token response instead of hard-coding any of these numbers.


Token rotation

Diagram: for public clients each use of a refresh token issues a new access token and a new refresh token, invalidating the old one; reusing an already-rotated token is treated as a replay and revokes the entire token family, forcing the user to sign in again — confidential clients keep one long-lived refresh token instead

Public clients receive a new refresh token on every use. The old one is immediately invalidated.

If an already-rotated token is reused, the server detects a replay attack and revokes the entire token family: all access tokens and refresh tokens from that authorization.


Storing tokens

WhatWhereWhy
Access tokenMemory (JS variable)Not persisted, invisible to other tabs and scripts
Refresh tokensessionStorageSurvives page reloads; gone on tab close
code_verifiersessionStorageSingle-use; delete immediately after the callback
statesessionStorageSingle-use; delete immediately after the callback

Avoid localStorage. Tokens persist forever and are accessible to any script on the page.


CORS policy

The token endpoint adds CORS headers for public clients only (token_endpoint_auth_method: none). Your app’s origin must match the scheme, host, and port of a registered redirect URI.

Example: registering https://myapp.example.com/callback allows requests from https://myapp.example.com.

Confidential clients never receive CORS headers — the browser blocks the response. Call the token endpoint from your backend instead.

The discovery endpoint has open CORS (Access-Control-Allow-Origin: *).


Troubleshooting

invalid_grant on the token endpoint

Three causes account for almost every case:

CauseCheck
Code expired or already usedAuthorization codes live 10 minutes and work once. Retrying a token request replays the code — don’t
code_verifier mismatchThe verifier must be the exact string whose SHA-256 you sent as code_challenge. A new verifier generated on the callback page is the classic bug
redirect_uri mismatchThe redirect_uri in the token request must equal the one in the authorization request, character-for-character

Refresh suddenly fails and API calls return 401

This applies to public clients only. If a refresh token is used twice — because of a race between tabs, a retry on timeout, or a restored backup — the server treats it as a replay attack and revokes the entire token family: every access token and refresh token from that authorization. From your side it looks like a working integration that abruptly gets invalid_grant on refresh and 401 on API calls.

There is no partial recovery. Send the user through the full /oauth/authorize flow again. To avoid it: serialize refresh calls behind a single lock, and always replace the stored refresh token the moment a new one arrives.

Confidential clients are different: they keep one long-lived refresh token, its idle TTL resets on every use, and reuse is expected — no rotation, no family revocation.

CORS errors calling the token endpoint from a browser

The token endpoint adds CORS headers only for public clients (token_endpoint_auth_method: none), and only for origins that match the scheme, host, and port of a registered redirect URI. If your page is served from an origin you never registered (a preview deploy, a different port), the preflight fails. Register a redirect URI on that origin, or proxy the token call through your backend.

Confidential clients never receive CORS headers on the token endpoint — that’s by design. Call it from your backend, not the browser.

The discovery endpoint is open CORS — if discovery works but the token call doesn’t, it’s an origin mismatch, not a network problem.

How do I revoke a token?

There is no /oauth/revoke endpoint. To “log out”, discard the tokens on your side — the access token expires within 60 minutes and the refresh token expires when its TTL runs out. Server-side revocation happens automatically on replay detection, or by OnePageCRM admin action.

Where’s the userinfo endpoint?

There isn’t one. The aud field in every token response carries the user’s CRM API base URL — use it to route API calls. For anything else about who authorized, keep your own session state from before the redirect.


Security checklist

  • PKCE used on every authorization request (code_challenge_method: S256)
  • state verified in the callback before processing the code
  • Authorization code exchanged within 10 minutes
  • Each authorization code used only once
  • Refresh token replaced on every use (public clients)
  • Access token kept in memory, not localStorage
  • 401 from the API triggers re-login, not a silent failure
  • redirect_uri uses HTTPS in production
  • error parameter in callback URL handled and shown to the user