This document describes how clients authenticate to Eliza Cloud APIs, how CORS is configured, how rate limiting works, and the canonical JSON error shape.
- Edge skips Privy for API-key-shaped requests so handlers can validate keys against the database (why not validate keys at the edge: avoid duplicating DB logic, permissions, and rate limits in middleware).
- Handlers choose
requireAuth*(cookies only) vsrequireAuthOrApiKey*(cookies + keys + wallet rules) per route (why both: browsers and automation are first-class; some flows must stay human-session-only for abuse and UX reasons). - Session-only lists at the edge return
session_auth_requiredwhen a client sendsX-API-KeyorBearer eliza_…to a cookie-only route (why: clear errors instead of passing the edge then failing inside the handler).
For a longer rationale (CLI session public split, crypto GET/POST, key management), see auth-api-consistency.md.
- Cookie:
privy-token(and related Privy cookies). - Used by: Dashboard and browser flows via
getCurrentUser()/requireAuth()/requireAuthWithOrg(). - Edge: Non-public routes require a valid Privy JWT unless another bypass applies (see Edge middleware).
- Session-only routes: Some endpoints reject
X-API-KeyandAuthorization: Bearer eliza_…at the edge and require a cookie session; see Session-only routes.
- Header:
X-API-Key: eliza_<secret>(prefix iseliza_). - Used by: Server and browser clients calling routes that use
requireAuthOrApiKey/requireAuthOrApiKeyWithOrg. - Edge: Requests with
X-API-KeyorAuthorization: Bearer eliza_…skip Privy verification in the proxy so the route handler can validate the key.
- Header:
Authorization: Bearer <token>. - JWT (Privy): Three-segment JWT verified as a session token; user loaded from DB.
- API key: Same value as
X-API-Keymay be sent as Bearer; keys are validated viaapiKeysService.validateApiKey.
- Headers:
X-Wallet-Address,X-Wallet-Signature,X-Timestamp(all required together). - Behavior: When all three are present,
requireAuthOrApiKeyattempts wallet verification only and does not fall through to API key or cookie auth (fail-closed). - Edge: Wallet signature passthrough is limited to specific path prefixes (e.g. topup / user wallets); see
proxy.ts.
- Prefix:
/api/internalis listed as a public path in the proxy (no Privy gate). - Auth: Bearer JWT validated by
validateInternalJWTAsync/withInternalAuthinpackages/lib/auth/internal-api.ts. - Purpose: Service-to-service calls (e.g. gateways); not end-user API keys.
- Flow:
validateAppAuthinpackages/lib/middleware/app-auth.ts— API key must be tied to an app; optional origin checks againstallowed_origins.
- Paths under
/api/webhooks,/api/cron,/api/v1/cron, provider callbacks, etc. are public at the edge; each handler verifies signatures, secrets, or tokens as appropriate.
publicPaths/publicPathPatterns: No Privy session required; handlers enforce their own auth. CLI login:POST /api/auth/cli-sessionandGET /api/auth/cli-session/:sessionId(poll) are matched by patterns;POST .../:sessionId/completeis not public so session-only / API-key rules apply at the edge.protectedPaths: Non-API paths (e.g./dashboard) redirect to login when unauthenticated.- Other
/api/*: Requires Privy cookie or Bearer JWT, or API-key style bypass as implemented inproxy.ts. sessionOnlyPaths/sessionOnlyPathPatterns: For these paths, requests that presentX-API-KeyorAuthorization: Bearer eliza_…receive 401 withcode: "session_auth_required"at the edge (cookie session required). Wallet-signature passthrough for allowed topup/wallet paths is unchanged.- OPTIONS for
/api/*: CORS preflight uses shared constants frompackages/lib/cors-constants.ts.
| Helper | Use when |
|---|---|
requireAuth() |
Cookie session only; anonymous users allowed per implementation. |
requireAuthWithOrg() |
Cookie session only; must have org. Use for handlers that must not accept API keys (e.g. signup-code redeem), or mixed files where one method stays session-only. |
requireAuthOrApiKey(request) |
Session or API key or wallet headers (with fail-closed wallet rules). |
requireAuthOrApiKeyWithOrg(request) |
Same as above but org required (typical for paid / credit usage). |
requireAdmin(request) |
Admin wallet + role via requireAuthOrApiKeyWithOrg then admin checks. |
API key management: List/create/update/delete/regenerate keys under /api/v1/api-keys accept API keys. Use a different key than the one you are modifying or revoking.
These are intentionally not usable with X-API-Key / Bearer eliza_… at the proxy (early 401 session_auth_required). Handlers use requireAuth() / requireAuthWithOrg() as documented in code.
| Area | Paths / notes |
|---|---|
| Post-login / CLI | /api/auth/migrate-anonymous, /api/auth/cli-session/:sessionId/complete |
| Invites (accept only) | /api/invites/accept — one-time user action |
| Promo / abuse | /api/signup-code/redeem |
| API Explorer UI key | /api/v1/api-keys/explorer |
| Dashboard LLM helpers | /api/v1/generate-prompts, /api/v1/character-assistant |
| Stripe checkout | /api/stripe/create-checkout-session |
| My agents | /api/my-agents/claim-affiliate-characters, /api/my-agents/characters/:id/track-interaction |
| Crypto confirm | /api/crypto/payments/:id/confirm |
| Crypto create | POST /api/crypto/payments — handler stays session-only; GET list accepts API keys |
Infrastructure, billing reads, voices, keys, org admin, profile:
/api/v1/dashboard,/api/v1/apps/:id/deploy,/api/v1/apps/:id/domains(+ status, sync, verify)/api/elevenlabs/voices(premade list),/api/elevenlabs/voices/user,/api/elevenlabs/voices/jobs,/api/elevenlabs/voices/:id,/api/elevenlabs/voices/verify/:id/api/v1/api-keys,/api/v1/api-keys/:id,/api/v1/api-keys/:id/regenerate/api/v1/user(GET/PATCH; org optional per user record)/api/sessions/current,GET /api/crypto/payments,GET /api/crypto/payments/:id/api/organizations/members,/api/organizations/members/:userId,/api/organizations/invites,/api/organizations/invites/:inviteId
- Shared allow list:
CORS_ALLOW_HEADERS,CORS_ALLOW_METHODS,CORS_MAX_AGEinpackages/lib/cors-constants.ts— used bynext.config.ts,proxy.tsOPTIONS,packages/lib/middleware/cors-apps.ts,packages/lib/services/proxy/cors.ts, and reflected inpackages/lib/utils/cors.tsfor allowlisted origins. - Wildcard
*: Default for most/api/*responses; credentials are not used with*; auth is via headers or same-site cookies on the app origin. - Origin allowlist + credentials:
getCorsHeadersinpackages/lib/utils/cors.tsfor first-party domains that needAccess-Control-Allow-Credentials: true.
- Wrapper:
withRateLimit(handler, config)inpackages/lib/middleware/rate-limit.ts. - Storage: Redis when
REDIS_RATE_LIMITING=true(recommended in multi-instance production); otherwise in-memory per instance. - Keying: Prefers API key, then
x-privy-user-id, then anonymous session; seegetDefaultKeyin the same file. - Org burst (MCP / A2A):
ORGANIZATION_SERVICE_BURST_LIMIT(100 req / 60s) keys Redis asmcp:ratelimit[:slug]:orgIdanda2a:orgId. UseenforceMcpOrganizationRateLimitso 429 bodies matchwithRateLimit(success,code,message,retryAfter,X-RateLimit-*).
| Preset | Typical use |
|---|---|
STANDARD |
Default API traffic (60/min). |
STRICT |
Sensitive mutations (10/min). |
RELAXED |
High read throughput (200/min). |
CRITICAL |
Rare, expensive ops (5 per 5 min). |
BURST |
Per-second burst cap. |
AGGRESSIVE |
Public / unauthenticated; keyed by IP (100/min). |
429 response includes success: false, error, code: "rate_limit_exceeded", message, retryAfter, and rate-limit headers.
Canonical JSON for API errors (aligned with ApiError in packages/lib/api/errors.ts):
{
"success": false,
"error": "Human-readable message",
"code": "authentication_required | session_auth_required | rate_limit_exceeded | ...",
"details": {}
}details is optional. Proxy-generated 401 responses use code: "authentication_required" or, for session-only paths with API key / Bearer eliza_…, code: "session_auth_required".
Use errorToResponse(error) or jsonError(message, status, code) where a raw Response is needed. For MCP-style handlers that must use native Response (not NextResponse), apiFailureResponse(error) maps ApiError subclasses to the same toJSON() shape and status as the rest of the API. For App Router routes using NextResponse, nextJsonFromCaughtError(error) uses the same logic via shared caughtErrorJson(error).