API authentication
Catentio has three credentials in play. They are not interchangeable, and each opens a different surface.
If you are calling Catentio from your own code, you want an API key. Create one in the portal under API keys, then send it to the public API:
curl https://catent.io/v1/agents \ -H "Authorization: Bearer cat_pk_..."
The three credentials
| Credential | Verified by | Opens |
|---|---|---|
Authorization: Bearer cat_pk_... |
Control plane | The public API (catent.io/v1/*) — nine endpoints. |
x-catentio-session header |
Control plane | The internal control-plane routes, from a signed-in browser. |
Authorization: Bearer <huudis-jwt> |
Control plane | Billing, chat (most of it), and workspaces list/get/create/delete. |
API keys (cat_pk_...)
An API key authenticates you to the public API and nothing else.
- It is scoped to the workspace that created it. Every run and project you create through the public API belongs to that workspace, and you cannot read another workspace's — a run id you do not own returns
404, not403. - It reaches only the nine public endpoints. It does not open the internal control-plane routes, and it is refused outright by the runtime.
- Store it like a password. It is shown once, at creation.
- Revoking it in the portal takes effect within about 30 seconds.
An API key is not an admin credential. It cannot list integrations, read secrets, manage agents, or invoke tools. If you need those, you are looking for the portal, not the API.
Which one am I holding?
A cat_pk_ key talks to catent.io/v1/*. A browser session talks to the control plane through the portal's proxy at /api/v1/cp/<path> (which rejects Authorization: Bearer entirely). The two surfaces have paths that look alike and are not the same thing — see API reference → Reachability.
Portal session (x-catentio-session)
This is the credential the product actually runs on.
At sign-in the portal HMAC-signs a catentio_session cookie with SESSION_COOKIE_SECRET. Server components and the /api/v1/cp/* proxy read that cookie and forward its value to the control plane in an x-catentio-session header. The control plane verifies the HMAC and decodes the payload:
{
"huudisUserId": "user_01H...",
"huudisAccessToken": "eyJhbGc...",
"huudisRefreshToken": "...",
"accessExpAt": 1800000000000,
"customerId": "internal"
}
The cookie is httpOnly and Secure. You don't set the header by hand — the portal does it.
customerId is the tenant. Every route scopes its reads and writes to the customer_id on the verified principal; a client-supplied customer_id in a request body is discarded.
Huudis JWT (OIDC device flow)
Use device flow when the caller should authenticate as the same Huudis identity that signs into the portal. This is what the CLI does.
It only opens a subset of the API. The control plane accepts a Bearer JWT on:
/v1/billing/*(all of it)/v1/chat/send,/v1/chat/sessions,/v1/chat/sessions/{id}— but not/v1/chat/attachmentsor/v1/chat/call/turn/v1/workspaces— list, get, create, delete (but notPATCH /{id}/state, which is admin-only)
Everywhere else, a Bearer JWT gets 401 auth_required.
How it works
- Your client POSTs to Huudis's device-flow endpoint and receives a
device_code, auser_codeand averification_uri_complete. - The client shows the URL and code to the user.
- The user authenticates on Huudis.
- The client polls Huudis's token endpoint until it returns access + refresh tokens.
- The client presents the access token as
Authorization: Bearer <jwt>.
The Node SDK re-exports the primitives (startDeviceFlow, pollDeviceToken, refreshAccessToken, Session) from @forjio/sdk:
import {
CatentioSaasClient,
Session,
startDeviceFlow,
pollDeviceToken,
} from '@forjio/catentio-saas-node';
const flow = await startDeviceFlow({
issuer: 'https://huudis.com',
clientId: 'catentio-saas-cli',
});
console.log('Visit:', flow.verificationUriComplete);
console.log('Code: ', flow.userCode);
const tokens = await pollDeviceToken(flow);
const session = new Session({ ...tokens, issuer: 'https://huudis.com', clientId: 'catentio-saas-cli' });
const catentio = new CatentioSaasClient({ session });
Session holds the refresh token and re-mints access tokens before they expire.
The CLI's real defaults are issuer https://huudis.com, client id catentio-saas-cli, scope openid profile email catentio:admin, credentials at ~/.catentio-saas/credentials.
API key (cat_pk_...)
An API key is a runtime credential, not a control-plane credential. It is verified by the agent runtime's auth guard (trust class
customer_pat). Presenting one to the control plane does nothing — the control plane only readsx-catentio-sessionand, on a few routes, a Huudis JWT.
Minting a key
Portal → Dashboard → API keys → Create API key. You give it a name; the secret is shown once:
cat_pk_5fGZ8h2KqWnpL3vBxYsR6tAjMdC1eIuN
Copy it immediately. Only a SHA-256 hash and a short display prefix are stored, so it cannot be shown again.
Scopes are not implemented. The API accepts a scopes field on create and discards it (payload.pop("scopes", None)). Every key grants full access to its customer's data. Per-resource scopes are not shipped.
Using a key
Authorization: Bearer cat_pk_5fGZ8h2KqWnpL3vBxYsR6tAjMdC1eIuN
against a runtime you can reach. The runtime hashes the secret, looks it up in api_keys, rejects it if revoked_at is set, and stamps the key's customer_id on the request.
Rotation
Keys never expire on their own. To rotate: mint a new key, deploy it, then delete the old one. DELETE /v1/api-keys/{key_id} takes the key's integer id and soft-revokes it (the row survives for audit); the control plane checks you own the key first and returns 403 not_your_key otherwise. Nothing signed with a revoked key succeeds afterwards.
Never embed an API key in client-side code. It carries full access to your workspace's data.
Who may sign in
The portal gates sign-in against an allow-list: HUUDIS_ALLOWED_USER_ID (the internal operator) plus the optional comma-separated HUUDIS_ALLOWED_USER_IDS (customer users). An identity outside that list is rejected by /api/v1/auth/login with 403 { "error": { "code": "NOT_AUTHORIZED" } }.
There is no forbidden_user error code, and Catentio is no longer single-user — workspaces, per-tenant runners, billing and customer onboarding are all shipped. See Authentication overview.
Errors
| Status | detail |
Means |
|---|---|---|
| 401 | auth_required |
No credentials presented (or a Bearer JWT on a session-only route). |
| 401 | malformed_session |
Session value wasn't <body>.<sig>. |
| 401 | invalid_signature |
Session HMAC didn't verify. |
| 401 | malformed_payload |
Session body wasn't valid JSON. |
| 401 | invalid_bearer: <code> |
Huudis rejected the JWT (expired, wrong audience, revoked). |
| 401 | admin_required |
Admin-only route without a valid admin assertion. |
| 403 | internal_only |
Internal-tenant-only route (/v1/gojo/*) called by a customer. |
| 403 | not_your_key |
Tried to revoke someone else's API key. |
Next
- API reference — response shapes, limits, resource index.
- Authentication overview — the portal-facing sign-in flow.
- SDK overview — what the SDKs can and can't reach today.