---
updatedAt: 2026-04-27T20:07:03.000Z
---

# Authentication Guide

Understand the three supported authentication methods: OAuth2 client credentials for server-to-server integrations, Supabase JWT tokens for dashboard users, and API key + public client ID for public routes.

> **Environment tip:** All examples use `v3.onsched.com` for production. Replace the host with `api-stage.onsched.com` when calling the staging environment.

> **Migrating from v1:** Older integrations used `identity.onsched.com` (OpenID Connect). v3 APIs use **`v3.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](#token-refresh)).        |
| A public booking widget or unauthenticated site   | **API key** + **public client ID** on `/v3/public/*` 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-api-key: <company key>` + `x-client-id: <public client id>`  | Obtain both from the v3 dashboard. Used for `/v3/public/*` endpoints that don't require user authentication. |

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 and public routes, `x-api-key` must match that company.

### Headers at a glance

* `Authorization: Bearer <token>` — OAuth2 access tokens (one hour) or dashboard JWTs.
* `x-api-key` — Scopes the request to a company. Required for dashboard JWTs and public routes; optional for machine tokens because the company is embedded in the token.
* `x-client-id` — Public client ID for `/v3/public/*` routes. Safe to expose in client apps but useless without a valid API key.
* Rotate client secrets and API keys regularly; treat API keys as confidential except when used alongside public client IDs for unauthenticated booking flows.

## OAuth2 Client Credentials Flow

1. Generate a client ID/secret pair in the dashboard (`POST /v3/clientId`; see the [API reference](https://docs.onsched.com/docs/swagger-docs)).
2. Request an access token:

```bash
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"
```

3. Use the `access_token` as a Bearer token for one hour. Tokens embed `company_id`, `grant_type=client_credentials`, and optional scopes.
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

1. Authenticate via the v3 dashboard to receive a Supabase JWT.
2. Include two headers on every API request:
   * `Authorization: Bearer <JWT>`
   * `x-api-key: <Company API key>`
3. The API cross-checks that the JWT user belongs to the company linked to the API key.

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.

## Public Routes Flow

1. Obtain your company's API key and public client ID from the v3 dashboard.
2. Include both headers on every public API request:
   * `x-api-key: <Company API key>`
   * `x-client-id: <Public client ID>`
3. These credentials authenticate the company but do not identify a specific user. All public routes are scoped to `/v3/public/*` endpoints.

Use this flow for customer-facing booking widgets, public availability displays, and other scenarios where end users don't have dashboard accounts.

## 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](#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 JWT, or invalid public client ID.
* **User not associated:** Returned when `x-api-key` belongs to a company the current dashboard user is not a member of.
* **Invalid Client ID:** Returned when the `x-client-id` header is missing, invalid, or expired (for public routes).
* **Scope violations:** Client-credentials tokens may include a space-delimited `scope` string; your integration should only rely on scopes you requested at token issuance.

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, ensure your public client ID is properly configured in the dashboard and hasn't expired.