Authentication
Every request to Cortex carries a bearer credential that resolves to a principal — either a user (a human) or a service account (an app, integration, or background job). Credentials are opaque strings sent in the Authorization header.
Principals: users and service accounts
Cortex has exactly two kinds of principals. Every credential resolves to one of them, and every request has exactly one actor.
| Principal | Who | Authenticates with | Scope |
|---|---|---|---|
| User | A human in an organization. | A session JWT obtained by logging in, or by an app minting one on the user's behalf (see Sessions below). | Can see and act on user-personal data (own threads, own drafts) plus shared org data. |
| Service account | A non-human caller owned by the organization — an in-house app, an automation, a background job. | A long-lived API key. | Can act on shared org data. To act on user-personal data, it mints a session as a specific user. |
API keys are credentials, not identities. Each key belongs to one service account. Permissions (role, admin access, cross-org mint) live on the service account, so you can rotate keys (issue a new one, revoke the old one) without changing what the principal can do or who audit trails attribute the activity to.
Get an API key
API keys are created from the developer dashboard. There is no public API for minting them — keys are issued through the UI so that a human is always on the loop when a new credential is created.
- Sign in at
https://developers.cortex.cognit-dx.com. - Open the API keys section.
- Click Create, give the key a descriptive name, and choose a role.
- Copy the plaintext value.
Behind the scenes, creating a key also creates a service account that owns it (one SA per key by default). You can attach additional keys to the same service account later to rotate without losing the identity — see Rotate and revoke.
The plaintext key is shown once, at the moment of creation. After you close the dialog the server only stores a hash — the key cannot be retrieved again. If you lose it, revoke it and create a new one under the same service account.
Store the key in a secret manager or an environment variable. Never commit it to source control and never embed it in client-side code that ships to end users.
Use an API key
Send the key in the Authorization header on every request, prefixed with Bearer.
curl https://api.cortex.cognit-dx.com/v1/threads \
-H "Authorization: Bearer $CORTEX_TOKEN"Treat the key as an opaque string. Do not parse it, do not decode it, do not branch on its prefix or length. The exact format is an implementation detail and may change without breaking the contract.
Keys must travel in the Authorization header only. Never place them in URL query strings or path segments — they will appear in server logs, proxy logs, and browser history.
Acting as a user
Sometimes a service account needs to act as a specific user — for example, an HR app authenticates an employee through its own SSO and then needs to call Cortex so the resulting threads, drafts, and usage are attributed to that employee.
The pattern is session minting:
- The app, holding its service-account API key, posts to
POST /v1/auth/sessionswith the user's email and password (or, for SSO-fronted apps, the equivalent verified identifier). - Cortex returns a short-lived JWT (default 8 hours) along with its
expires_at, scoped to that user. - The app uses that JWT as the bearer credential for subsequent requests on behalf of the user. Shortly before it expires (or after a
401), the app refreshes it silently withPOST /v1/auth/sessions/renew— posting the current token together with the API key — so the user is never interrupted. The API key plays the role of a refresh credential.
A session JWT cannot mint or renew a session on its own — only an API key can. Renewal requires both the server-side API key and a cryptographically genuine prior token, and a token left idle past the renew window (7 days) must sign in again. This prevents a stolen short-lived token from being renewed indefinitely without the long-lived credential.
Rotate and revoke
Because keys belong to service accounts (not the other way around), rotation keeps the same identity:
- Create a new key under the same service account in the dashboard.
- Update every consumer (services, scripts, CI secrets) to use the new key.
- Revoke the old key.
Audit logs, usage attribution, and permissions all remain pointed at the same service account through the rotation. Revocation is immediate: the next request made with a revoked key returns 401. There is no grace period and no cached validity window. Roll new keys forward before you revoke the old one so no consumer is left holding an invalid credential.
To disable a service account entirely — for example, while investigating a leak — suspend it via the dashboard or admin API. All of its keys stop working immediately, including any sessions minted from them.
Revoke a key whenever it may have leaked, whenever a team member with access leaves, or as part of a regular rotation schedule.
Change your password
A signed-in user can change their own password with POST /v1/users/me/password. The request must carry a session JWT (not an API key) and include the current password plus a new password of at least 12 characters that differs from the current one. A successful change returns 204 No Content.
Session tokens are stateless and are not derived from the password, so changing it does not sign out existing sessions; they remain valid until they expire.
Console roles
Platform routes (everything under /v1/admin) gate on a capability flag, can_access_console, carried by the credential — the same flag as API keys and sessions above. Passing that gate gets you in the door; which sections of the console you can actually see and act on is a second, finer-grained check.
A user with role: "admin" is granted every section — this is unchanged legacy behavior. A non-admin user can instead hold a console role, which maps to a specific set of sections (platform, organizations, sales, operations, ai-platform, billing, access). Today two roles are seeded:
| Role | Sections |
|---|---|
| Operator | All seven sections — equivalent day-to-day access to an admin. |
| Sales | sales only. |
Console roles are assigned per user via PATCH /v1/admin/organizations/:id/users/:userId (the consoleRoleId field) and are meaningful only for users of the system organization — the same assignment can also be made through the dedicated system-users surface below, which additionally supports suspension and password resets. See Organizations for the full field. Roles themselves are managed via GET/POST /v1/admin/console-roles and PATCH/DELETE /v1/admin/console-roles/:id — the list response includes each role's sections and a user_count of how many system-org users currently hold it. These management routes are themselves gated on the access section.
A signed-in user can read their own assignment from GET /v1/users/me, which returns console_role (the role name) and console_sections (the sections it grants) — both null when the user holds no console role. A denied platform request always comes back 403 with code: "admin_required", whether the caller lacks can_access_console entirely or simply holds a console role that doesn't cover the section being accessed.
A console role is required for system-organization members: creating a member without one, clearing a member's role, or demoting an admin who holds none is rejected with 400 validation_error (suspension is the tool for revoking access), and a role must grant at least one section. The rule is also enforced at sign-in: when the API key presenting POST /v1/auth/sessions (or /sessions/renew) carries can_access_console, a member with no console role — or a role granting zero sections — is refused with 403 and code: "no_console_role" plus a user-facing message, after the password check so the endpoint stays a non-oracle. Keys without console access (the member frontend) are unaffected. The console shows that message on the login form, so a role-less account sees an explicit explanation instead of a silent redirect loop.
The sales section, held by the seeded Sales role, grants exactly the demo-organizations routes — everything under /v1/admin/demo-organizations — and nothing else; a Sales-only user gets 403 admin_required on every other /v1/admin route, including the plain organizations endpoints. See Demo organizations for the full endpoint list.
GET /v1/admin/system-users, PATCH /v1/admin/system-users/:id, and POST /v1/admin/system-users/:id/password offer a dedicated, org-scoped surface for managing console access on the caller's own organization — the system organization, verified server-side. The list response includes each user's console_role_id and console_role (the role name, or both null). The PATCH body accepts console_role_id (a role id or null to clear it; an unknown id is 400) and is_suspended — self-suspension is rejected with 409. The password endpoint mirrors POST /v1/admin/organizations/:id/users/:userId/password: omit password to have one generated, returned once in the response body. All three routes are gated on the access section and 404 for any user outside the caller's organization.
Errors
Authentication uses two distinct error statuses; see Errors for the full code catalog.
| Status | When |
|---|---|
401 | The Authorization header is missing or malformed, the credential is unknown, revoked, or expired, or the service account owning it has been suspended. The client should present a new credential. On session mint and renew specifically, a suspended or expired organization also fails here (code: "invalid_credentials") rather than as a 403 — those two endpoints never reveal an organization's suspension state ahead of authentication. |
403 | The credential is valid and the caller is authenticated, but the identity is not permitted to perform this action on this resource. Every other route — the request already carries a live session — returns 403 with code: "org_suspended" instead of succeeding when the caller's organization is suspended or, for a demo organization, past its expiry. |