API Authentication

Every call to the Atanzo AI external API — including minting a session credential — starts with an OAuth2 access token. Get this right once in your backend and every other integration (guided sessions, coaching presets, agent config, media) uses the same token.

Before you can request a session credential or call any other endpoint, you need three things: an OAuth2 client (an api_key/api_secret pair issued for your integration), the scopes granted to that client (which endpoints and session modes it's allowed to use), and a server to hold the credentials — the API is designed so secrets never reach the browser. This page covers the token endpoint, the two grant types you'll actually use, how scopes gate endpoints and session modes, and the backend-for-frontend pattern that keeps everything server-side.

Getting a Token — POST /oauth

All tokens come from a single endpoint:

POST /oauth

grant_type is required on every request; the endpoint supports client_credentials, password, and refresh_token. As a partner integration you'll use one of the first two — refresh_token exists for the internal Cognito-console path and isn't relevant to embed integrations (embed tokens don't carry a refresh token; just request a new one before the current one expires).

Client Credentials — anonymous / partner deployments

Use this when your integration acts as the partner company itself, with no individual end-user identity — the typical case for an anonymous embed deployment (a microsite, kiosk, or any surface where every visitor shares the same credential).

Request:

{
  "grant_type": "client_credentials",
  "api_key": "your-api-key",
  "api_secret": "your-api-secret"
}

Response:

{
  "success": true,
  "access_token": "eyJhbGciOiJSUzI1NiIs...",
  "token_type": "Bearer",
  "expires_in": 3600,
  "cid": "company-123",
  "scope": "invoke_workflow invoke_voice invoke_video",
  "scopes": ["invoke_workflow", "invoke_voice", "invoke_video"]
}

The token resolves to your company (cid) — every call made with it is scoped to that company, not to an individual person. scopes reflects what your API key was provisioned with; if your key record has no explicit scopes, it falls back to the deployment's default scope set.

Password — authenticated embed deployments

Use this when your integration needs the session tied to a specific person rather than the company as a whole — for example, a coaching app where each logged-in user has their own run history. This grant takes both your partner API key and that end-user's own Cognito credentials:

Request:

{
  "grant_type": "password",
  "api_key": "your-api-key",
  "api_secret": "your-api-secret",
  "username": "end-user@example.com",
  "password": "end-users-password"
}

Response:

{
  "success": true,
  "access_token": "eyJhbGciOiJSUzI1NiIs...",
  "token_type": "Bearer",
  "expires_in": 3600,
  "cid": "company-123",
  "euid": "a1b2c3d4-...",
  "scope": "invoke_workflow invoke_voice invoke_video",
  "scopes": ["invoke_workflow", "invoke_voice", "invoke_video"]
}

The extra euid claim is what makes this different from client_credentials — it identifies the specific platform end-user the token was minted for, and it's enforced server-side: the end-user must already be associated with your cid or the request is rejected (403 CROSS_TENANT_DENIED). Downstream, euid lets per-end-user coaching modules and run history work correctly.

Both grants issue a 1-hour access token (expires_in: 3600). Neither returns a refresh token — when it's close to expiring, just call /oauth again. A minimal client-side pattern is to cache the token and its expiry and re-request a little before it actually expires (see Never Mint Client-Side below).

Scopes and Cognito Groups

Every access token carries a set of scopes. A custom API Gateway authorizer validates the token on every request and checks the caller's scopes against what the endpoint requires. Embed tokens (client_credentials/password) carry scopes directly as a JWT claim, provisioned per API key; internal Cognito users get scopes resolved from their Cognito group membership. system_admin and api_admin groups bypass all scope checks.

ScopeGrantsCognito Group
invoke_workflowStandard/text workflow invocation via POST /invoke. Does not grant voice or video guided sessions.invoke_adminInitiateWorkflow
invoke_voiceVoice guided sessions (POST /guide/session with mode: "voice")invoke_guided_voice
invoke_videoVideo guided sessions (POST /guide/session with mode: "video")invoke_guided_video
manage_schedulesCreate, edit, delete schedulesmanage_workflow_invocations
read_schedulesView schedulesread_workflow_invocations
manage_agentsCreate, edit, delete agent configurationsmanage_agent_configs
read_agentsView agent configurationsread_agent_configs
manage_mediaUpload, edit, delete media filesmanage_media_library
read_mediaView and download media filesread_media_library

Per-Mode Enforcement on Guided Sessions

POST /guide/session gets special treatment because the authorizer only sees the HTTP method and path — it can't read the request body, so it can't tell a voice request from a video one at the gate. Instead, the authorizer admits any caller holding either invoke_voice or invoke_video (a coarse "some guided scope" check) and passes the caller's full granted scope set downstream as context.

The guidedSessionToken Lambda then does the precise check once it can see the body: mode: "video" (or a coaching preset whose device_mode is video) requires invoke_video; mode: "voice" requires invoke_voice. If your token doesn't carry the specific scope the requested mode needs, you'll get:

{
  "error": "SCOPE_REQUIRED",
  "message": "This credential lacks the 'invoke_video' permission required for video sessions",
  "success": false
}

with HTTP status 403. Practically: if you only ever run voice sessions, an invoke_voice-only key is fine — provision invoke_video too before you flip a deployment over to camera input, or session creation will start failing at this gate. (There's a second, independent gate behind this one — a per-company plan-module entitlement — so having the right scope is necessary but not always sufficient; that gate fails with a different error code and isn't something your API key alone controls.)

Never Mint Client-Side

The api_key/api_secret (and, for the password grant, the end-user's own credentials) must never reach the browser. They're long-lived, deployment-wide secrets — anyone who extracts them from client-side JavaScript can mint tokens as your company. The only thing that's safe to hand the browser is the short-lived, single-session SessionCredential returned by POST /guide/session, which is scoped to one LiveKit room and expires with the session.

The pattern is a small backend-for-frontend layer: your server holds the OAuth credentials as environment variables, exchanges them for an access token, caches that token until shortly before it expires, and uses it to call /guide/session on the end-user's behalf. The browser only ever talks to your own backend and receives the resulting credential — never the OAuth token, never the API key. The Atanzo AI demo gallery's own microsites are built exactly this way: a small server-side module fetches and caches the client_credentials token, and every session request goes through it rather than calling /oauth or /guide/session from client code. Model your own integration's backend on that same shape regardless of which grant type you use.

Error Codes

CodeHTTP StatusMeaning
MISSING_GRANT_TYPE400grant_type was omitted
MISSING_API_CREDENTIALS400api_key/api_secret missing from a client_credentials or password request
MISSING_CREDENTIALS400username/password missing from a password request
INVALID_CREDENTIALS401API key not found, inactive, expired, or api_secret mismatch
AUTHENTICATION_FAILED / INVALID_CREDENTIALS401End-user's Cognito username/password rejected (password grant)
USER_NOT_FOUND403End-user has no platform account (password grant)
CROSS_TENANT_DENIED403End-user exists but isn't associated with your cid (password grant)
SCOPE_REQUIRED403Token lacks the scope the requested session mode needs (see above)
UNSUPPORTED_GRANT_TYPE400grant_type isn't client_credentials, password, or refresh_token

Next Steps