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
- Your client POSTs to Huudis's device-flow endpoint and receives a
device_code+user_code+ averification_uri_complete. - The client prints the URL and code to the user (or opens a browser tab).
- The user authenticates on Huudis with the user code.
- The client polls Huudis's token endpoint; once the user finishes, polling returns access + refresh tokens.
- 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.
Portal cookie (browser only)
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
- API overview — envelope, errors, rate limits.
- Authentication overview — how the portal-facing flow works.
- SDK overview — idiomatic auth across Node / Python / Go.