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, not 403.
  • 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/attachments or /v1/chat/call/turn
  • /v1/workspaces — list, get, create, delete (but not PATCH /{id}/state, which is admin-only)

Everywhere else, a Bearer JWT gets 401 auth_required.

How it works

  1. Your client POSTs to Huudis's device-flow endpoint and receives a device_code, a user_code and a verification_uri_complete.
  2. The client shows the URL and code to the user.
  3. The user authenticates on Huudis.
  4. The client polls Huudis's token endpoint until it returns access + refresh tokens.
  5. 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 reads x-catentio-session and, on a few routes, a Huudis JWT.

Minting a key

Portal → Dashboard → API keysCreate 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