Authentication Guide
Understand supported authentication: OAuth2 client credentials, dashboard JWTs, public client ID plus Allowed Domain, and Calendar Selection Tokens during hosted external calendar setup.
Environment tip: All examples use
v3.onsched.comfor production. Replace the host withapi-stage.onsched.comwhen calling the staging environment.
Migrating from v1: Older integrations used
identity.onsched.com(OpenID Connect). v3 APIs usev3.onsched.com(or staging). Server-to-server access should use OAuth2 client credentials on/v3/oauth/token—not the dashboard refresh-token endpoints below.
Which flow do I need?
| If you are building… | Use this |
|---|---|
| A backend or cron job that calls the API | OAuth2 client credentials (/v3/oauth/token). Only client_id, client_secret, and short-lived access tokens—no API key step, no refresh token. |
| Something that acts as a logged-in dashboard user | Dashboard JWT + x-api-key. Optional refresh path only if you are managing a Supabase-style session (see Token refresh). |
| A public booking widget or unauthenticated site | Public client ID + matching Allowed Domain (Origin) on /v3/public/* only. |
| Hosted external calendar selection after OAuth | Calendar Selection Token from POST /v3/calendar/selectionSession for list/select/disconnect on that connection only. |
Do not chain API key → refresh token → access token for machine integrations—that mixes different models. API keys are not part of the OAuth2 client-credentials flow.
When to Use
- Secure server-to-server integrations that call the API directly.
- Configure dashboard-powered apps that rely on Supabase-issued JWTs.
- Enable public-facing booking flows that don't require user authentication.
- Rotate credentials without interrupting production traffic.
Auth Models
| Use Case | Headers Required | How to Obtain |
|---|---|---|
| OAuth2 Clients (recommended for API consumers) | Authorization: Bearer <access_token> | Exchange client_id and client_secret via POST /v3/oauth/token using the client_credentials grant. |
| Dashboard Users | Authorization: Bearer <user JWT> + x-api-key: <company key> | Retrieve from the v3 dashboard. Tokens map to specific users and expire according to Supabase settings. |
| Public Routes | x-client-id: <public client id> + matching Origin | Obtain the public client ID from the v3 dashboard and register the page host as an Allowed Domain. Used for /v3/public/* only. |
| Calendar Selection (opt-in guest picker) | Authorization: Bearer <Calendar Selection Token> | Connect with useOnSchedCalendarSelection: true, then after delegated OAuth exchange the callback code via POST /v3/calendar/selectionSession. Use the returned token on GET …/externalCalendars/list, POST …/externalCalendars/select, and DELETE …/externalCalendar/:id for that resource connection until it expires (~15 minutes). Not a substitute for machine or dashboard tokens on other routes. |
For OnSched-hosted calendar selection, your application only creates the opted-in connection and opens its returned authUrl. OnSched's picker handles code exchange and selection-token use automatically. Customers do not generate or supply CALENDAR_SELECTION_JWT_SECRET; OnSched manages that infrastructure secret. See the External Calendar Sync Guide for a complete request example.
After authentication succeeds, the request is scoped to the correct company and user context. For machine tokens, the company comes from the JWT; for dashboard JWTs, x-api-key must match that company. Public routes identify the company from x-client-id and allow the call only when Origin matches that company's Allowed Domain list.
Headers at a glance
Authorization: Bearer <token>— Machine Token (OAuth2 access token, one hour) or Dashboard JWT. Not a refresh token or Docs Handoff Token. Hosted calendar setup endpoints also accept a Calendar Selection Token (~15 minutes).x-api-key— Scopes a dashboard JWT to a company. Optional for machine tokens because the company is embedded in the token. Not required on/v3/public/*.x-client-id— Public client ID for/v3/public/*routes. Safe to embed in browser apps. The browserOriginmust match an Allowed Domain for that company.- Rotate client secrets and API keys regularly. Treat API keys as confidential. Public client IDs are visible in booking widgets by design.
OAuth2 Client Credentials Flow
- Generate a client ID/secret pair in the dashboard (
POST /v3/clientId; see the API reference). - Request an access token:
curl -X POST https://v3.onsched.com/v3/oauth/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-u "<client_id>:<client_secret>" \
-d "grant_type=client_credentials&scope=read write"- Use the
access_tokenas a Bearer token for one hour. Tokens are HS256 JWTs withissonsched:v3-api,audapi,company_id,grant_type=client_credentials, and OAuth Scopes. The JSON body also returns the grantedscopestring (read,write, orread write).
Only read and write are valid OAuth Scopes. Omit scope (or send a blank value) to receive both. Unknown names—including mixed lists such as read admin—return 400 Invalid OAuth scope and do not issue a token. A write token may GET; a read token cannot POST, PUT, PATCH, or DELETE.
Machine tokens operate only within their authenticated company. Another company's resource ID returns 404; a foreign related ID in a request body or query returns 400. Read/write scopes still apply. Supplying another company ID or API key does not switch the token's company.
Company creation, dashboard profile changes, migration workflows, support-ticket operations, and dashboard refresh-token generation require a dashboard user token. Machine tokens receive 403 for these operations. Use GET /v3/companies to retrieve the machine token's bound company.
4. Rotate secrets periodically; revoke compromised credentials by deleting them via DELETE /v3/clientId/:id.
Machine tokens do not require x-api-key or x-client-id headers—the access token already identifies the company.
Dashboard Token Flow
- Authenticate via the v3 dashboard to receive a Supabase Dashboard JWT (access token).
- Include two headers on every API request:
Authorization: Bearer <JWT>x-api-key: <Company API key>
- The API cross-checks that the JWT user belongs to the company linked to the API key.
A Dashboard JWT is not a Machine Token. It is also not the long-lived refresh JWT from POST /v3/auth/generateRefreshToken — exchange that refresh token at POST /v3/auth/token and send the returned access token as Bearer.
Use this flow when you need user-level auditing (for example, when exposing the API directly from your dashboard session). For backend integrations, prefer OAuth2.
The merchant portal can also sign an operator into Personalized Docs after login (POST /v3/auth/readmeAuth). That Docs Handoff Token identifies the operator in docs only. It cannot call /v3/*. It does not prefill Try It — paste a Dashboard JWT plus API Key, or a Public Client ID, from company settings.
Public Routes Flow
- Obtain your company's public client ID from the v3 dashboard and register each booking-page hostname as an Allowed Domain (for example
www.example.com). Do not use*. - Include
x-client-id: <Public client ID>on every public API request. Browsers sendOriginautomatically; that value must match an Allowed Domain for the same company (httporhttps, exact host). - These credentials identify the company but do not identify a dashboard user. All public routes are scoped to
/v3/public/*endpoints. An extrax-api-keyheader is ignored.
Use this flow for customer-facing booking widgets, public availability displays, and other scenarios where end users don't have dashboard accounts. Scripted callers without Origin receive 403.
Token refresh
Two different endpoints—do not confuse them:
| Endpoint | Purpose |
|---|---|
POST /v3/oauth/token | Machine / client credentials only. Same as OAuth2 Client Credentials Flow. Returns an access token. No refresh token. When it expires (~1 hour), call again with the same client_id and client_secret. |
POST /v3/auth/token | Dashboard session only. Body: { "refresh_token": "..." }. Exchanges a Supabase refresh token for a new access token (and may return a rotated refresh token). Not used for OAuth2 client credentials. |
Details:
- OAuth2: Access tokens expire after about one hour. Request a new access token with the same client credentials; no refresh token is issued.
- Dashboard: Owners may call
POST /v3/auth/generateRefreshToken(authenticated with a valid Bearer token) to mint a long-lived refresh token for integrations that mirror a user session. Store refresh tokens in a secret manager or secure storage—not in a cache with aggressive eviction unless you have another way to recover them. - Front-ends: Do not expose client secrets or long-lived refresh tokens in the browser; proxy token exchange through your backend.
Error Handling
- 401 Unauthorized: Missing or invalid headers, malformed Basic auth, expired or unverifiable JWT, or invalid public client ID. A refresh token or Docs Handoff Token is not a valid API Bearer.
- 400 Bad Request:
POST /v3/oauth/tokenwith an unknown OAuth Scope name returnsInvalid OAuth scope. - 403 Forbidden: Public
Originis missing or is not an Allowed Domain for that company. A machine token missing the OAuth Scope required for the HTTP method returnsInsufficient scope: write requiredorInsufficient scope: read required. - User not associated: Returned when
x-api-keybelongs to a company the current dashboard user is not a member of. - Invalid Client ID: Returned when the
x-client-idheader is missing, invalid, or expired (for public routes). - Scope violations: Request only
readand/orwrite. Rely on thescopefield in the token response.
Always store secrets in a secure vault and avoid embedding them in client-side code. Use short-lived OAuth2 tokens for any automation or integration work. For public routes, register Allowed Domains in the dashboard and send a public client ID from a matching page origin.
Updated 7 days ago
