Skip to main content
Every public request carries an API key in the X-API-Key header. The gateway resolves the key to a user and injects identity downstream — you never pass a user id or subject yourself; the server derives it from the key.

Base URL

Getting a key

Don’t have a key yet? Hosted: sign in at vexa.ai/signin with a Google account and copy your key from your account page — free credit, no card required. Self-hosted: make all prints a key when the stack comes up. make all mints a key as part of bring-up and prints it (along with the service URLs) when the stack is ready — copy it from the make all output and use it as your X-API-Key.

Minting more keys

To mint additional keys, use the provision-token make target with your ADMIN_TOKEN (it’s set in .env, default dev-admin-tokenchange it before exposing anything):
Under the hood the admin API mints keys. It listens on http://localhost:18057 by default (ADMIN_API_PORT) and is protected by ADMIN_TOKEN via the X-Admin-API-Key header — you can call it directly for finer control over users and scopes:
The returned token is your X-API-Key. It is shown once — store it.

Scopes

A key carries one or more scopes. Pass them in the JSON body as {"scopes":["bot","tx"]}, or as query scope=<one> / scopes=<a>,<b>. The body wins when both are present. An unknown body field is refused with 422 — never silently dropped. /recordings*, /user/webhook*, /user/models, /user/transcription, /agent/* and /mcp accept either bot or tx. Most keys want both: {"scopes":["bot","tx"]}. The three /user/* routes in that list are the Settings API — model credentials, transcription backend, and webhook delivery, stored per key owner. Every route requires a scope. A key that holds none of the scopes a route accepts gets 403, whichever route it is. Keys are prefixed by their primary scope — vxa_bot_…, vxa_tx_…, vxa_browser_…. A key without a recognized scope is rejected.

Rotating and revoking

Mint a new key, switch your clients over, then delete the old one by its token id:
Set an expiry at mint time with expires_in=<seconds>; expired keys are rejected automatically.

Login-minted tokens (terminal-login)

When a user signs in through the terminal (OAuth or email login), the terminal mints an API token named terminal-login to populate the auth cookie. These are bounded per user: after each sign-in the terminal keeps only the newest VEXA_TERMINAL_LOGIN_TOKEN_CAP (default 3) and revokes the older ones. So when you audit a user’s tokens —
— the terminal-login-named entries are login sessions (capped, not unbounded), while any differently-named entries are self-serve keys the user minted and are never pruned by login.

What can go wrong

See the full error reference.