Authentication
How to authenticate CognitiveX API, SDK, and browser calls with API keys, session cookies, the icog.app handoff, and OAuth.
Every request to https://api.cognitivx.io resolves to a single user (and,
when relevant, an org). There are three ways to prove who you are:
- An API key (
icog_...) for servers, scripts, CI, agents, and the MCP server. - A JWT access token issued by sign-in, for the same
/apiroutes. - Session cookies set by the global SSO at
auth.cognitivx.io, for browser apps.
API and SDK integrations almost always want an API key. Browser apps on a
cognitivx.io subdomain get cookies for free. The sections below cover each,
plus the icog.app cross-origin handoff and OAuth sign-in.
There is no path versioning. All product endpoints live under the unversioned
/api prefix (for example /api/recall). There is no Accept-Version header.
Which should I use
API key
Server-side code, scripts, CI, autonomous agents, the MCP server. Long-lived, revocable, scoped to one user. This is the default for integrations.
JWT access token
You already ran a sign-in flow and hold a short-lived access token. Same
/api routes as an API key. Expires; refresh or re-sign-in when it does.
Session cookies
A browser app served from a *.cognitivx.io subdomain. The SSO cookie is
sent automatically with credentials: 'include'. No token handling.
OAuth + handoff
You are building a sign-in UI, or bridging a session to icog.app (a
different registrable domain that cookies cannot reach).
Quick rule: if your code runs on a server, use an API key. If it runs in a
browser on cognitivx.io, use cookies. Only reach for the handoff or raw OAuth
when you are building the auth surface itself.
API keys
An API key is the simplest credential: a long-lived string scoped to one user. Create keys at developers.cognitivx.io/keys. The full key is shown once, at creation. Store it in a secret manager, never in client-side code.
A key looks like icog_ followed by a URL-safe token. Only the SHA-256 hash and
the first 8 characters (key_prefix, for example icog_abc) are stored, so a
lost key cannot be recovered. Revoke and reissue instead.
Sending a key
Both header forms are accepted and equivalent:
curl https://api.cognitivx.io/api/me \
-H "X-API-Key: icog_YOUR_KEY"A value starting with icog_ is treated as an API key whether you send it via
X-API-Key or Authorization: Bearer. Use whichever your HTTP client makes
easiest.
Keep API keys server-side. A leaked key has the full access of the user it belongs to until you revoke it. Revocation is immediate.
Managing keys over the API
You can also mint, list, and revoke keys programmatically. These endpoints are authenticated with a JWT access token (a logged-in user session), not with an API key, since they manage credentials for a user.
Create a key
Mint a personal API key. The plaintext key is returned only on this response.
POST /api/auth/api-keys
Auth: Authorization: Bearer <JWT access token>
| Field | Type | Required | Description |
|---|---|---|---|
name | string | yes | A label to recognize the key later (for example prod or ci). |
curl -X POST https://api.cognitivx.io/api/auth/api-keys \
-H "Authorization: Bearer YOUR_JWT" \
-H "Content-Type: application/json" \
-d '{"name": "prod"}'import { keys } from "@cognitivx/sdk";
const created = await keys.create({ name: "prod" });
console.log(created.key); // icog_... shown once, store it now{
"id": "8f3c2b1a-1e4d-4a9c-9b2e-7c1d2f3a4b5c",
"name": "prod",
"key_prefix": "icog_abc",
"created_at": "2026-06-16T08:30:00Z",
"last_used_at": null,
"key": "icog_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
}Errors — 401 if the JWT is missing, expired, or invalid. 422 if name
is missing.
List keys
GET /api/auth/api-keys
Auth: Authorization: Bearer <JWT access token>
Returns each key's metadata. The full key is never returned after creation, so
key is absent here; use key_prefix and name to identify keys.
curl https://api.cognitivx.io/api/auth/api-keys \
-H "Authorization: Bearer YOUR_JWT"import { keys } from "@cognitivx/sdk";
const all = await keys.list();
all.forEach((k) => console.log(k.name, k.key_prefix, k.last_used_at));[
{
"id": "8f3c2b1a-1e4d-4a9c-9b2e-7c1d2f3a4b5c",
"name": "prod",
"key_prefix": "icog_abc",
"created_at": "2026-06-16T08:30:00Z",
"last_used_at": "2026-06-16T09:14:51Z"
}
]Revoke a key
DELETE /api/auth/api-keys/{key_id}
Auth: Authorization: Bearer <JWT access token>
| Path param | Type | Required | Description |
|---|---|---|---|
key_id | string (UUID) | yes | The id of the key to revoke. |
curl -X DELETE https://api.cognitivx.io/api/auth/api-keys/8f3c2b1a-1e4d-4a9c-9b2e-7c1d2f3a4b5c \
-H "Authorization: Bearer YOUR_JWT"import { keys } from "@cognitivx/sdk";
await keys.revoke("8f3c2b1a-1e4d-4a9c-9b2e-7c1d2f3a4b5c");Returns 204 No Content. Revocation is immediate.
Errors — 404 if no key with that id belongs to you.
Org context
Keys and tokens resolve to a user, and a user can belong to organizations. To
act inside an org with a JWT, send X-Org-ID: <org-uuid>; membership is
checked and a non-member gets 403. Without the header, calls fall back to your
active org. An org-scoped API key always forces its own org, so you do not
send X-Org-ID with one.
JWT access tokens
A successful sign-in (POST /api/auth/signin, OAuth, or the handoff) yields a
short-lived JWT access token. It authenticates the same /api routes as an API
key:
curl https://api.cognitivx.io/api/recall \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiI..." \
-H "Content-Type: application/json" \
-d '{"query": "billing decisions", "limit": 5}'Access tokens expire (401 "Token expired"). When they do, mint a fresh one
with POST /api/auth/refresh using your refresh-token cookie, or sign in again.
For non-interactive code that should not expire, use an API key instead.
SSE streaming endpoints (for example /api/talk/stream) accept the token as a
?token= query parameter because the Cloudflare proxy in front of them does not
forward custom auth headers on event streams.
Session cookies
Browser apps served from a cognitivx.io subdomain (console, developers, docs,
and so on) do not handle tokens. The global SSO at auth.cognitivx.io sets an
httponly refresh-token cookie scoped to the shared cognitivx.io parent
domain, so every sibling subdomain is signed in at once.
Send cross-subdomain fetch calls with credentials and the cookie rides along:
await fetch("https://api.cognitivx.io/api/me", {
credentials: "include",
});The cookie is SameSite=Lax, Secure, and HttpOnly, so JavaScript cannot
read it. It is sent automatically; you never copy it into a header. Cookie auth
only works within *.cognitivx.io. For a different registrable domain such as
icog.app, use the handoff below.
OAuth providers
auth.cognitivx.io/login supports sign-in with Google, GitHub, and
X (Twitter). The backend drives a standard OAuth 2.0 authorization-code flow
(with PKCE/S256 for X). You normally do not call these endpoints directly; the
hosted login page does. They are documented here for teams building a custom
sign-in surface.
Start the flow
Get the provider consent URL, then redirect the browser to it.
GET /api/auth/oauth/{provider}/authorize
Auth: none (public).
| Path param | Type | Required | Description |
|---|---|---|---|
provider | string | yes | One of google, github, twitter. |
curl "https://api.cognitivx.io/api/auth/oauth/google/authorize"{
"authorize_url": "https://accounts.google.com/o/oauth2/v2/auth?client_id=...&redirect_uri=...&state=...",
"redirect_uri": "https://api.cognitivx.io/api/auth/oauth/google/callback"
}Redirect the user to authorize_url. The state parameter is a signed,
short-lived token that carries your originating origin and (for X) the PKCE
verifier, so you do not manage either yourself.
Errors — 400 if the provider is unknown or not configured on the server.
Handle the callback
After the user consents, the provider redirects back to
GET /api/auth/oauth/{provider}/callback?code=...&state=.... The backend
exchanges the code, finds or creates the matching user, sets the refresh-token
cookie, and issues a 302 redirect to your frontend at:
/auth/callback?token=<access_token>&new=<0|1>Your frontend reads token (the JWT access token) and new (1 for a freshly
created account, 0 for an existing one) off the query string. From there you
hold a normal access token and, on cognitivx.io, the session cookie.
You do not implement the token exchange, the PKCE challenge, or user creation.
Calling authorize and handling the callback redirect is the whole
integration.
The icog.app handoff
Cookies scoped to cognitivx.io cannot be read by icog.app, which is a
separate registrable domain. To carry an existing session across that boundary,
the backend mints a one-shot token on the source origin and exchanges it for a
cookie on the target origin.
Mint a handoff token
Call this from a page that already has a session cookie (the dependency requires
an authenticated request). The token is bound to target_origin, is single-use,
and expires in 60 seconds.
POST /api/auth/handoff/mint
Auth: session cookie (an authenticated request on the source origin).
| Field | Type | Required | Description |
|---|---|---|---|
target_origin | string | yes | The origin that will exchange the token. Must be on the allowlist (for example https://icog.app). |
curl -X POST https://api.cognitivx.io/api/auth/handoff/mint \
-H "Content-Type: application/json" \
--cookie "refresh_token=YOUR_SESSION_COOKIE" \
-d '{"target_origin": "https://icog.app"}'{ "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." }Errors — 400 if target_origin is not on the allowlist. 401 if the
request is not authenticated.
Exchange it on the target origin
The target app POSTs the token back. The request's Origin header must match
the origin the token was minted for, or it is rejected. On success the backend
sets the standard session cookie for the target origin.
POST /api/auth/handoff/exchange
Auth: none (the token is the credential). The Origin header must match the
token's audience.
| Field | Type | Required | Description |
|---|---|---|---|
token | string | yes | The token returned by mint. |
curl -X POST https://api.cognitivx.io/api/auth/handoff/exchange \
-H "Content-Type: application/json" \
-H "Origin: https://icog.app" \
-d '{"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."}'{
"user_id": "c2d4e6f8-1234-5678-9abc-def012345678",
"email": "[email protected]",
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}The response carries an access_token for immediate use, and the response also
sets the refresh-token cookie on the target origin so subsequent requests are
authenticated by cookie.
Errors — 400 if the token is missing, expired, malformed, already used,
the Origin does not match the token, or the target is not allowed.
The handoff token is single-use and lives 60 seconds. Mint it immediately before the redirect and exchange it as soon as the target page loads. Do not log, cache, or reuse it.
Errors
Authentication failures share the standard error shape. Every error body is
{"detail": ...}, and every response carries an X-Request-ID header you can
quote in a bug report.
| Status | When |
|---|---|
401 | Missing, invalid, or expired credential. Messages include Invalid API key, Could not validate credentials, and Token expired. |
403 | Authenticated but not allowed: email not verified, tier too low, or org non-member. detail is a structured object for these gates. |
422 | A request body failed validation (for example creating a key without name). |
429 | The auth endpoints (signup, signin, forgot-password, reset-password, resend-verification) are rate-limited to 5 requests per minute per IP: {"detail": "Too many requests. Please try again later."}. |
There is no Idempotency-Key support and no X-RateLimit-* or Retry-After
headers on the auth limiter. Do not build retry logic around headers that are
not sent.
What's next
- Run your first authenticated call.
- Browse the API reference for every endpoint.
- Plug iCog into Claude or Cursor with the MCP tools.