Authentication
Authenticate with user access tokens or workspace API keys, and the scopes each key can hold.
opendesk accepts two kinds of bearer credential.
| Credential | For | Header |
|---|---|---|
| Access token | The web app and anything acting as a signed-in user | Authorization: Bearer <jwt> plus OD-Workspace |
| API key | Servers, scripts and integrations | Authorization: Bearer od_live_... |
The chat widget uses a third, narrower credential, the visitor token (Authorization: Visitor od_vis_...). See Widget API.
Whichever you use, the workspace comes from the credential: an API key’s own workspace, or the OD-Workspace a signed-in member belongs to. A workspace id in a request body never widens what a request can reach, and a record in another workspace answers 404, the same as one that does not exist.
API keys
Section titled “API keys”API keys are the right choice for integrations. Create one in Settings → API keys (admin only) with a name, a set of scopes and an optional expiry. The secret is shown once, when the key is created. opendesk stores only a hash of it.
curl https://api.opendesk.sh/v1/contacts \ -H "Authorization: Bearer od_live_3q2...xYz"- A key is bound to one workspace.
OD-Workspaceis ignored. - The key list shows each key’s prefix, scopes, creator, expiry and when it was last used.
- Deleting a key revokes it immediately. Creating and deleting keys is audit-logged.
Scopes
Section titled “Scopes”| Scope | Grants |
|---|---|
tickets:read |
Read tickets, their messages and activities, and related ticket data |
tickets:write |
Create, update and reply to tickets |
contacts:read |
Read contacts and companies |
contacts:write |
Create, update and delete contacts and companies |
kb:read |
Read knowledge base collections and articles, including drafts |
kb:write |
Create, update and delete collections and articles |
settings:read |
Read workspace settings, such as the workspace, members, teams, mailboxes and webhooks |
settings:write |
Change workspace settings |
realtime |
Get a realtime token for the workspace |
Give each key only the scopes it needs. A request outside a key’s scopes returns 403 with code forbidden.
Access tokens
Section titled “Access tokens”Access tokens are short-lived JWTs (15 minutes) issued when a user signs in.
POST /v1/auth/loginwith{ "email", "password" }returns{ access_token, expires_in, user }and sets a refresh cookie. If the account has two-factor authentication, it returns{ "mfa_required": true, "mfa_token" }instead; send that with the code toPOST /v1/auth/mfa.- Send
Authorization: Bearer <access_token>andOD-Workspace: <id>on each request. - Before it expires,
POST /v1/auth/refreshwith the cookie returns a new access token. The refresh token rotates on every use. POST /v1/auth/logoutrevokes the session and closes its live sockets.
The refresh token lives in a cookie named after the account, od_rt_<user id> (HttpOnly, Secure, SameSite=Lax, scoped to /v1/auth), so it is never readable by page scripts.
Several accounts
Section titled “Several accounts”A browser can be signed in to up to five accounts at once, each with its own refresh cookie. Signing in to another account leaves the others signed in; a sixth answers 409 too_many_accounts.
POST /v1/auth/refreshwith{ "user_id" }refreshes that account only. Without it, the account used most recently.POST /v1/auth/accountslists the accounts this browser is signed in to, with their workspaces, without rotating anything.POST /v1/auth/logoutsigns out of the current account only;{ "all": true }signs out of every account in the browser.
Cookies from earlier versions (od_refresh) keep working and move to the new name on first use.
Sign-in is rate limited and protected by a challenge, so for automation use API keys rather than scripting a login.
Native apps
Section titled “Native apps”The desktop app is a browser window onto the web app and uses cookies like any browser. Apps without a trusted cookie store, such as the iPhone app, use native sessions:
- A request is native when it sends
OD-Client: ios/<version>and noOriginheader. Browsers always sendOriginonPOST, so page scripts can never receive a refresh token. - To a native request, every endpoint that starts a session (register, login,
mfa,oauth/exchange, passkey login,refresh) returnsrefresh_tokenin the body and sets no cookie.POST /v1/auth/refreshand/v1/auth/logouttake{ "refresh_token" }in the body. The token rotates on every use, as the cookie does. - Where the web sends a Turnstile token, a native iPhone client may send an
app_attestproof instead (Apple App Attest, accepted for the app ids inAPP_ATTEST_APP_IDS). Get a single-use challenge fromPOST /v1/auth/app-attest/challengeand register the attested key withPOST /v1/auth/app-attest/keys. - OAuth sign-in runs in the system browser with PKCE (
client=native&code_challenge=...on/start) and returns throughopendesk://auth/callback;oauth/exchangethen needs thecode_verifier.
The full flow, payloads and checks are in contracts/native.md in the repository.
Credential formats
Section titled “Credential formats”| Credential | Format | Lifetime |
|---|---|---|
| Access token | JWT, HS256, purpose: access |
15 minutes |
| Realtime token | JWT, purpose: ws |
5 minutes, only needs to be valid at connect |
| API key | od_live_ + 32 random bytes, base64url |
until revoked or its expiry |
| Visitor token | od_vis_ + 32 random bytes, base64url |
the widget session |
Every JWT carries a purpose, and each consumer accepts exactly one, so a token issued for one use cannot be replayed for another.