Authentication
There are exactly two ways to authenticate: a session cookie for anything running in a browser, and a scoped API key for anything running on a server.
Session authentication (browser)
Signing in sets an HttpOnly, Secure, SameSite=Lax session cookie. Because the cookie travels automatically, every state-changing request must also carry a CSRF token. Fetch it once per page load:
curl -i https://<your-api-host>/api/v1/ping --cookie-jar jar.txt{ "ok": true, "csrf_token": "csrf_3f19...", "user": { "id": "usr_1", "role": "owner" } }Send the token as X-CSRF-Token on every POST, PUT, PATCH and DELETE. From JavaScript, include credentials on every call:
await fetch(`${API}/leads/lead_a1`, {
method: "PATCH",
credentials: "include",
headers: {
"Content-Type": "application/json",
"X-CSRF-Token": csrfToken,
"X-Request-Id": crypto.randomUUID(),
},
body: JSON.stringify({ status: "working" }),
});Access-Control-Allow-Credentials: true. If you would rather not manage that, proxy the API through your own server and keep the cookie same-origin.API keys (server)
Keys look like cak_live_7Kd2… and are shown once, at creation. Send them as a bearer token:
curl https://<your-api-host>/api/v1/opportunities \
-H "Authorization: Bearer cak_live_7Kd2..."Keys never need a CSRF token. Never ship a key to a browser, a mobile app, or a public repository — a leaked key can read every call in the workspace it belongs to.
Scopes
Each key carries an explicit list of scopes. Requests outside them return 403.
| Field | Type | Description |
|---|---|---|
| calls:read | scope | List calls, read a call and its transcript. |
| opportunities:read | scope | List and read scored acquisition opportunities. |
| opportunities:write | scope | Assign an owner, change the outcome, add desk notes. |
| leads:read | scope | List and read leads and contacts. |
| leads:write | scope | Create leads, update status and notes. |
| agent:read | scope | Read the agent draft and the published configuration. |
| agent:write | scope | Replace the draft and publish it. |
| webhooks:manage | scope | Create, update and delete webhook endpoints. |
Roles
Session users act within their role. A key can never exceed the role of the member who created it.
| Field | Type | Description |
|---|---|---|
| owner | role | Everything, including billing, deletion and transferring ownership. |
| admin | role | Agent editing and publishing, team invitations, keys and webhooks. |
| member | role | Works the desk: calls, opportunities, leads, follow-ups. |
| viewer | role | Read-only across calls, opportunities and leads. |
Tenancy
Every record belongs to one workspace. Requesting a record from another workspace returns 404 not_found, not 403, so existence cannot be inferred from status codes.
