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.
| Scope | Grants | Cognito Group |
|---|---|---|
invoke_workflow | Standard/text workflow invocation via POST /invoke. Does not grant voice or video guided sessions. | invoke_adminInitiateWorkflow |
invoke_voice | Voice guided sessions (POST /guide/session with mode: "voice") | invoke_guided_voice |
invoke_video | Video guided sessions (POST /guide/session with mode: "video") | invoke_guided_video |
manage_schedules | Create, edit, delete schedules | manage_workflow_invocations |
read_schedules | View schedules | read_workflow_invocations |
manage_agents | Create, edit, delete agent configurations | manage_agent_configs |
read_agents | View agent configurations | read_agent_configs |
manage_media | Upload, edit, delete media files | manage_media_library |
read_media | View and download media files | read_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
| Code | HTTP Status | Meaning |
|---|---|---|
MISSING_GRANT_TYPE | 400 | grant_type was omitted |
MISSING_API_CREDENTIALS | 400 | api_key/api_secret missing from a client_credentials or password request |
MISSING_CREDENTIALS | 400 | username/password missing from a password request |
INVALID_CREDENTIALS | 401 | API key not found, inactive, expired, or api_secret mismatch |
AUTHENTICATION_FAILED / INVALID_CREDENTIALS | 401 | End-user's Cognito username/password rejected (password grant) |
USER_NOT_FOUND | 403 | End-user has no platform account (password grant) |
CROSS_TENANT_DENIED | 403 | End-user exists but isn't associated with your cid (password grant) |
SCOPE_REQUIRED | 403 | Token lacks the scope the requested session mode needs (see above) |
UNSUPPORTED_GRANT_TYPE | 400 | grant_type isn't client_credentials, password, or refresh_token |
Next Steps
- Getting a Session Credential — Use your access token to call
POST /guide/sessionand mint aSessionCredential - API & SDK Overview — Package map and architecture overview
- Reference Implementations — See the backend-for-frontend pattern in a working demo