API authentication

The Catentio control plane accepts three credential shapes. This page covers all three, with worked examples and guidance on which to pick for your context.

TL;DR. Use OIDC device flow for CLI / interactive scripts that should authenticate as you. Use a static API key (cat_ak_...) for server-to-server callers (cron jobs, integrations, your own backend). The portal's HMAC-signed session cookie is browser-only and you generally won't set it by hand.

The three paths

Header Auth model When to use
Authorization: Bearer <huudis-jwt> OIDC device flow — same Huudis identity as the portal CLI, interactive scripts, "do something as me"
Authorization: Bearer <cat_ak_...> Static API key minted in the dashboard Server-to-server, cron, integrations
x-catentio-session Portal HMAC-signed cookie Browser only — auto-set by the portal

The control plane tries them in the order above. The first one present wins; the resolved Principal is the same shape regardless of which path you took, and the single-user gate (in v1) applies equally to all three.

OIDC device flow (Bearer JWT)

Use device flow when you want the caller to authenticate as the same Huudis identity that signs into the portal. This is what the CLI uses by default.

How it works

  1. Your client POSTs to Huudis's device-flow endpoint and receives a device_code + user_code + a verification_uri_complete.
  2. The client prints the URL and code to the user (or opens a browser tab).
  3. The user authenticates on Huudis with the user code.
  4. The client polls Huudis's token endpoint; once the user finishes, polling returns access + refresh tokens.
  5. The client stores the tokens and presents the access token as Authorization: Bearer <jwt> on every API call.

Via SDK

The Node SDK exports the device-flow primitives directly:

import {
  CatentioSaasClient,
  Session,
  startDeviceFlow,
  pollDeviceToken,
} from '@forjio/catentio-saas-node';

const flow = await startDeviceFlow({
  issuer: 'https://huudis.com',
  clientId: 'catentio-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-cli',
});

const catentio = new CatentioSaasClient({ session });

const agents = await catentio.agents.list();

The Session keeps the refresh token internal and proactively re-mints access tokens before they expire, so a long-running process never 401s mid-flight.

The Python and Go SDKs expose the same primitives with idiomatic names — see their respective overview pages.

Via raw curl

If you've already done the device-flow dance manually (or you're scripting the CLI), the access token goes straight on the Authorization header:

curl https://catent.io/v1/agents \
  -H "Authorization: Bearer eyJhbGciOiJSUzI1NiIs..."

The control plane validates the JWT against Huudis's JWKS, extracts the sub claim, and matches against HUUDIS_ALLOWED_USER_ID.

Static API key

Use a static API key when:

  • The caller isn't you — it's your backend, a cron job, or an integration.
  • You want a credential whose lifetime you control (rotation, revocation) independent of any Huudis session.
  • You want zero round-trips to Huudis on every call.

Minting a key

Sign in to the portal. Navigate to Dashboard → API keys. Click Create API key.

The dialog asks for:

  • Name — human-readable (e.g., "production-cron", "linksnap-integration").
  • Scope — in v1 this is * (full workspace access). Per-resource scopes land in a later release.

Click Create. The dialog shows the secret once:

cat_ak_5fGZ8h2KqWnpL3vBxYsR6tAjMdC1eIuN

Copy it immediately into your secret manager. If you close the dialog without copying, you have to delete the key and mint a new one.

Using a key

Pass it as a Bearer token:

curl https://catent.io/v1/agents \
  -H "Authorization: Bearer cat_ak_5fGZ8h2KqWnpL3vBxYsR6tAjMdC1eIuN"

Or via SDK:

const catentio = new CatentioSaasClient({
  apiKey: process.env.CATENTIO_API_KEY,
});
from catentio_saas import CatentioClient

with CatentioClient(api_key=os.environ["CATENTIO_API_KEY"]) as catentio:
    agents = catentio.agents.list()
client, _ := catentio.NewClient(catentio.ClientOptions{
    APIKey: os.Getenv("CATENTIO_API_KEY"),
})

Rotation

API keys don't expire on their own. Rotate them:

  • When a team member with access leaves.
  • When you suspect compromise.
  • On a calendar schedule (we recommend quarterly).

Rotation = mint a new key, deploy it, then delete the old one from the dashboard. No request signed with the old key will succeed after deletion.

Never embed an API key in client-side code. It grants full workspace access — anyone with the key can invoke agents and read every run. Keep keys server-side.

The portal sets a catentio_session cookie at sign-in that's HMAC-signed with the portal's SESSION_COOKIE_SECRET. When a browser-originated API call hits the control plane, the portal forwards the cookie value in an x-catentio-session header, and the control plane verifies the HMAC + decodes the payload to derive the principal.

You don't set this header by hand. The portal sets it automatically when its server components or route handlers call the control plane.

Cookie payload (HMAC-SHA256 over the body):

{
  "huudisUserId": "user_01H...",
  "huudisAccessToken": "eyJhbGc...",
  "huudisRefreshToken": "...",
  "accessExpAt": 1800000000000,
  "customerId": "internal"
}

The cookie is httpOnly (no JavaScript can read it) and Secure (HTTPS-only).

Errors

Status Code Means
401 auth_required No credentials presented.
401 invalid_signature Session cookie HMAC didn't verify.
401 malformed_session Cookie shape was wrong.
401 <huudis error> Bearer JWT failed Huudis validation (expired, wrong audience, revoked).
403 forbidden_user Auth succeeded but the principal isn't the v1 allowlisted user.

Next