Authentication
aeqi has two distinct authentication surfaces:
| Surface | Token | Used by |
|---|---|---|
| User session | JWT bearer | Dashboard, in-browser REST, normal /api/* calls |
| Programmatic | API keys (ak_… + sk_…) |
MCP clients (Claude Code, Codex), automation scripts |
JWTs come from a login flow; API keys come from the dashboard.
User Session (JWT)
The dashboard and most /api/* endpoints expect a JWT bearer token:
Authorization: Bearer <jwt>
The token is signed with the platform's auth_secret. Claims include user_id,
email, and standard exp/iat.
Sign up
POST /api/auth/signup
{
"email": "you@example.com",
"password": "...",
"invite_code": "DOGFOOD-2026-XYZ"
}
Returns { ok: true, token: "<jwt>", pending_verification: true }. The session is unverified until the 6-digit email code is consumed.
POST /api/auth/verify
{ "email": "you@example.com", "code": "123456" }
Returns a fresh, verified JWT.
POST /api/auth/resend-code
Requests a new 6-digit code if the original was lost. Same rate-limited bucket as signup.
Log in
Password:
POST /api/auth/login
POST /api/auth/login/email
{ "email": "you@example.com", "password": "..." }
Returns { ok: true, token: "<jwt>" }. The two routes share a handler; /login/email is the historical alias.
If the account has TOTP enabled, the initial response is { ok: true, totp_required: true, partial_token: "..." }; finish with:
POST /api/auth/totp/login
{ "partial_token": "...", "code": "123456" }
Wallet (SIWE)
POST /api/auth/wallet/nonce
POST /api/auth/wallet/login
POST /api/auth/wallet/signup
nonce issues a per-address nonce; login/signup consume a SIWE message signed against that nonce and return a JWT.
Passkey
POST /api/auth/passkey/login-begin
POST /api/auth/passkey/login-finish
POST /api/auth/passkey/register-begin
POST /api/auth/passkey/register-finish
WebAuthn ceremony; finish returns a JWT on success.
Passwordless email
POST /api/auth/login/code/request
POST /api/auth/login/code/consume
POST /api/auth/login/magic/consume
A single login-codes row backs all three. Either the typed 6-digit code or the clicked magic link consumes it; consuming any path invalidates the row.
Recovery and identity changes
POST /api/auth/forgot-password
POST /api/auth/reset-password
POST /api/me/email/change/begin
POST /api/me/email/change/finish
Selecting a TRUST
After login, the JWT authenticates the user. When a request needs to target a
specific TRUST runtime — anything proxied through /api/{*rest} — the platform
reads X-Trust:
X-Trust: <trust_id>
The proxy also accepts trust or trust_id query parameters for browser flows
that cannot set headers easily. If no TRUST is supplied, proxied runtime calls
return 400. Calls to non-tenant routes (/api/keys, /api/billing/*,
/api/trusts, etc.) do not need X-Trust.
Token lifecycle
JWTs are bearer tokens. The active sessions list and revocation are:
GET /api/auth/sessions
POST /api/auth/sessions/revoke
POST /api/auth/sessions/revoke-others
There is no refresh endpoint — log in again to mint a new token.
Programmatic Access (API keys)
For MCP and automation. Two keys, each with a distinct role:
| Key | Prefix | Scope | Header |
|---|---|---|---|
| Secret key | sk_… |
One Company | Authorization: Bearer sk_… |
| Account key | ak_… |
Per user account | X-Api-Key: ak_… |
The secret key authenticates the call against a specific TRUST (user_id and
trust_id resolved from the key). The account key is optional but recommended
for MCP — when present, the platform asserts that the secret key belongs to the
same user, which guards against accidentally using a sk_... from a different
account.
API keys require a company or owner placement tier. Trial and free accounts cannot mint or use them; calls return 402 Payment Required with "MCP access requires a Company subscription".
Create an account key
POST /api/account/api-key
Authenticated with JWT. Mints (or rotates) the user's ak_…. Response:
{ "ok": true, "id": "...", "api_key": "ak_...", "rotated": false }
A user has at most one account key at a time; calling again returns the existing key with rotated: true and revokes the old value.
Create a secret key
POST /api/keys
Authenticated with JWT. Body:
{ "root": "<entity_name_or_id>", "name": "claude-code" }
Response:
{ "ok": true, "id": "...", "secret_key": "sk_...", "note": "Save the secret_key — it won't be shown again." }
The plaintext is returned only once. Store it immediately.
List and revoke
GET /api/keys
DELETE /api/keys/{id}
list returns metadata only — name, id, masked prefix, created/last-used timestamps. delete revokes a key immediately.
Use the keys
MCP client (Codex, Claude Code, etc.):
export AEQI_API_KEY=ak_...
export AEQI_SECRET_KEY=sk_...
export AEQI_PLATFORM_URL=https://app.aeqi.ai
aeqi mcp reads both and sets Authorization: Bearer sk_… plus
X-Api-Key: ak_… on every JSON-RPC call.
Direct MCP call:
curl https://app.aeqi.ai/api/mcp \
-H "Authorization: Bearer sk_..." \
-H "X-Api-Key: ak_..." \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
Validate a key
POST /api/mcp/validate
Echoes back which user/entity a key resolves to. Use this from a CI smoke test to verify a deployed secret without firing a tool call.
Rotation
POST /api/keysto mint a new secret with the sameroot.- Update your env or config.
DELETE /api/keys/{old_id}.
Account keys (ak_…) are identifiers — call POST /api/account/api-key to rotate; the old value stops working immediately.
Headers Summary
| Header | Used by | When |
|---|---|---|
Authorization: Bearer <jwt> |
Dashboard, in-browser REST | After a login flow |
Authorization: Bearer sk_… |
MCP, automation | When calling on a Company's behalf |
X-Api-Key: ak_… |
MCP, automation | Optional — pairs sk_ with the account |
X-Trust: <trust_id> |
REST | Selects target TRUST for proxied calls |
X-Company is not used.
Tiers
| Tier | Dashboard | MCP / API keys |
|---|---|---|
| Trial | Yes | No |
| Starter | Yes | No |
| Company | Yes | Yes |
| Owner | Yes | Yes |
See Billing for current pricing.
Next Steps
- REST API — endpoint reference
- MCP — tool catalog and JSON-RPC shape
- Claude Code + aeqi — IDE setup walkthrough