Authentication
API keys, sessions, OAuth for MCP connectors, workspaces and roles.
Every request carries a bearer credential in the Authorization header. There are three kinds:
| Credential | Looks like | Use it for | Workspace |
|---|---|---|---|
| API key | ro_pat_... | Scripts, integrations and MCP clients | Implied by the key |
| Session access token | a JWT | The web app (a signed-in person) | X-Workspace-Id header |
| OAuth access token | ro_oat_... | MCP connectors (Claude, ChatGPT, other MCP clients) | Chosen at consent |
API keys
Create keys in the app under Settings → API keys (only a signed-in person can create, list or revoke keys).
- Format:
ro_pat_plus 43 characters. The key is shown once, at creation; only its SHA-256 hash and a short display prefix are stored. Keys are never logged. - Identity: a key belongs to one workspace and acts as the person who created it. Every request re-reads that person's membership, so a key is capped by their current role: if they are demoted or removed, their keys lose that access too.
- Scopes: a key can only call routes its scopes allow. Granting
adminneeds the admin role. - Expiry: optional, from 1 to 3650 days (
expires_in_days); without it a key doesn't expire. - Workspace: implied by the key.
X-Workspace-Idis optional; if you send it, it must match the key's workspace (otherwise403). - Audit: every mutating request made with a key (HTTP or MCP), and every key creation or revocation, is recorded in the workspace's audit log.
curl "$ZEROREACH_URL/api/v1/leads" -H "Authorization: Bearer $ZEROREACH_API_KEY"Some routes never accept an API key, whatever its scopes: key management, the in-app assistant's routes, your own profile, password and sessions, whole-workspace privacy export and deletion, and approving or sending reply drafts (only a signed-in person does that). The API reference marks each route.
Errors
| Status | When |
|---|---|
401 | Invalid or expired API key. (with WWW-Authenticate: Bearer) |
403 | The key lacks the route's scope, the route doesn't accept API keys, or X-Workspace-Id names another workspace |
429 | Past the key's request rate (API_KEY_RATE_LIMIT_PER_MINUTE, default 600 per minute), or too many failed attempts from your IP |
Sessions (the web app)
The web app signs people in with POST /api/v1/auth/login (email and password). The answer holds a short-lived access token (expires_in, 15 minutes by default) and sets an HttpOnly refresh cookie (30 days by default); POST /api/v1/auth/refresh rotates it. Reusing a rotated refresh token revokes the whole session family.
A session token names no workspace, so requests that act on one must send X-Workspace-Id: <workspace uuid>:
- missing or not a UUID:
400; - a workspace you aren't a member of:
403 Not a member of this workspace.(the same answer whether or not it exists).
Signing out everywhere or changing the password revokes existing access tokens, and also the tokens of connected apps.
OAuth for MCP connectors
MCP clients that support the MCP authorization spec (for example Claude.ai custom connectors and ChatGPT connectors) can connect to /api/v1/mcp with OAuth instead of an API key:
- The client discovers the server from
/.well-known/oauth-protected-resourceand/.well-known/oauth-authorization-server(a401from the MCP endpoint names the metadata inWWW-Authenticate). - It registers itself (
POST /api/v1/oauth/register; public clients only; redirect URIs must behttps://or loopbackhttp://). - You sign in to ZeroReach, pick a workspace and approve on the consent screen. Authorization uses the code flow with PKCE (
S256). - The client gets an access token (
ro_oat_..., 1 hour by default) and a rotating refresh token (ro_ort_..., 30 days by default).
Connector tokens:
- carry the same scopes as API keys. Without a
scopeparameter a client gets every scope exceptadmin; - are capped by your current role, like an API key;
- are accepted only by the MCP endpoint.
Revoke a connection from the app, or with POST /api/v1/oauth/revoke. See Connect Zero via MCP.
Workspaces and roles
All customer data belongs to a workspace, and every query filters on it: another workspace's record behaves exactly like a missing one.
A person can belong to several workspaces, each with one role:
| Role | Can |
|---|---|
member | Use the workspace: leads, campaigns, inbox, calls, deals. |
admin | Everything a member can, plus workspace settings, members (admins and members), webhooks, discovery runs, signal rules, the audit log and other admin routes. |
owner | Everything, including managing owners. |
Rules for managing members:
- members can't manage anyone;
- admins manage admins and members;
- only owners can grant the owner role, or change or remove an owner;
- a workspace always keeps at least one owner.
Routes that need a higher role answer 403 Requires the <role> role or higher.