Skip to content

Authentication

Two kinds of credentials, for two kinds of callers.

Console users Applications
Credential Bearer token obtained with email and password API key ax_… created in the console
Used on Management API (/v1/users, /v1/llm, /v1/proxy-apps, …) Runtime API (/v1/proxy-apps/<app-id>/openai, /gemini, /vertex)
Authorization Role-based permissions Scope: one proxy app
Lifetime Until rotated; no expiry Optional expires_at; can be deactivated

Console users

Log in

curl -s http://localhost:3000/v1/users/login \
  -H 'Content-Type: application/json' \
  -d '{"email":"admin@axium.local","password":"admin123456"}'
{
  "status": true,
  "data": {
    "id": "00000000-0000-0000-0000-000000000002",
    "email": "admin@axium.local",
    "name": "Super Admin",
    "token": "9f1c…40 hex…",
    "is_active": true,
    "permissions_cache": ["core:read", "core:create", "…"],
    "last_login_at": "…", "created_at": "…", "updated_at": "…"
  }
}

Wrong credentials or an inactive user answer 401 Invalid email or password.

Use the token

curl http://localhost:3000/v1/users/me -H "Authorization: Bearer 9f1c…"

The token is opaque (not a JWT) and validated against the database on every request. It has no expiry. A new one is issued, invalidating the previous, on every login, on POST /v1/users/logout, when the account is deactivated and when an administrator resets the password.

Permissions

Each route declares the permissions that open it; the user needs at least one. Without a valid token: 401 Unauthorized. With a token but without the permission: 403 Forbidden. The list of permissions a user holds comes back in permissions_cache at login and in GET /v1/users/me.

Permissions are module:action:

Modules Actions
core (users, roles), llm (providers, models), rm (proxy apps, API keys, projects), observability (stats, traces), governance, chat, mcp, agents read, view, create, update, delete, manage, execute, test, export

The seeded super-admin role holds all of them. Create narrower roles with POST /v1/roles and PATCH /v1/roles/:id/permissions.

Change the initial password

curl -X PATCH http://localhost:3000/v1/users/password \
  -H "Authorization: Bearer <token>" -H 'Content-Type: application/json' \
  -d '{"current_password":"admin123456","new_password":"a-long-new-password"}'

Minimum 8 characters.

Applications: API keys

Create one

In the console, or:

curl -X POST http://localhost:3000/v1/rm/api-keys \
  -H "Authorization: Bearer <console token>" -H 'Content-Type: application/json' \
  -d '{"name":"billing-service","scope_type":"proxy_app","proxy_app_id":"<app-id>","expires_at":"2027-01-01T00:00:00Z"}'

The response contains key in clear once. Axium stores only a hash and the last four characters; a lost key has to be replaced.

Send it

Either header works; Authorization wins if both are present:

Authorization: Bearer ax_8d2d9ff993e24eb6144e8d87e7890a51401eca99
x-goog-api-key: ax_8d2d9ff993e24eb6144e8d87e7890a51401eca99

The second exists so Google's SDKs work unchanged. A ?key= query parameter is not accepted and is stripped before forwarding.

What the runtime checks

  1. The key exists, is active and has not expired. Any failure is the same 401, on purpose.
  2. The key is scoped to a proxy app, and it is the app in the URL. Otherwise 403.
  3. From there, the proxy app's own rules apply: status, budget, model allow-list, policies. See Proxy apps & gateways.

last_used_at is updated on a best-effort basis.

Rotate

Create the new key, deploy it, then deactivate the old one with PATCH /v1/rm/api-keys/:id {"is_active": false}. Deactivating keeps the key's history in statistics and traces; deleting does too but loses its name.