Authentication¶
All endpoints (except /api/health and /api/docs) require a credential.
Two credential shapes¶
The Authorization header accepts either of two token shapes, dispatched
on the prefix:
| Prefix | Kind | Minted by |
|---|---|---|
am_k1_ |
API key | appmecca api-key create (an administrator operation), or the environment's root key |
ses_ |
Session token | mecca login (also what the dashboard's session cookie translates to) |
A session token carries the signed-in user's identity, so it also resolves group membership and roles; an API key resolves to whichever group (if any) it is bound to.
Sending the credential¶
Include it in the Authorization header:
# With Bearer prefix (recommended)
curl -H "Authorization: Bearer am_k1_abc123..." https://api-url/api/apps
# Without Bearer prefix (also accepted)
curl -H "Authorization: am_k1_abc123..." https://api-url/api/apps
# A session token works the same way
curl -H "Authorization: Bearer ses_abc123..." https://api-url/api/apps
Selecting a group¶
Most app-scoped routes resolve which group you are acting as, in this order:
?group=<slug>query parameterX-AppMecca-Group: <slug>header- The group an API key is bound to
- Your personal group
A request for an app you cannot reach returns 404, never 403 — the API never confirms that another tenant's app exists.
Scope Hierarchy¶
Each key has a scope that determines what it can access:
| Scope | Level | Can Do |
|---|---|---|
admin |
3 | Everything — create keys, manage infrastructure, all operations |
deploy |
2 | Trigger builds, deploy apps, manage services, set env vars |
read |
1 | Read-only — list apps, view builds, check status |
Scopes are hierarchical: an admin key can do everything a deploy key can, and a deploy key can do everything a read key can.
Error Responses¶
401 Unauthorized¶
Returned when:
- No
Authorizationheader is present - The key doesn't have the
am_k1_prefix - The key hash doesn't match any active record in the database
- The key has been revoked (
revoked_at IS NOT NULL)
403 Forbidden¶
Returned when the key is valid but the scope is insufficient:
Key Storage¶
Keys are stored as SHA-256 hashes in the api_keys table; session tokens are hashed into the sessions table. The raw credential is never stored — only the prefix (for identification) and the hash (for validation).
The last_used_at timestamp is updated on every successful authentication.
Root Key Seeding¶
Each environment's root API key lives in the platform secret store. On first deployment it can be seeded into the api_keys table automatically.
WebSocket Tickets¶
A browser cannot attach an Authorization header (or read an HttpOnly session cookie) when opening a WebSocket. To authenticate a socket without exposing a credential to browser JavaScript, the API issues short-lived, single-use tickets. Every ticket is stored with a 60-second TTL and consumed atomically on first use, so a captured ticket is dead the moment it is redeemed.
There are two ticket flavours, for two different sockets.
POST /api/auth/deploy-ws-ticket — tenant deploy + preview events¶
Mints a ticket for your group's live deploy feed. Any authenticated member of the selected group may call it; the group is taken from the membership-checked resolution path (?group=<slug> 404s a non-member) and baked into the ticket at mint time, so a tenant can never subscribe to another group's channel.
{
"ticket": "<url-safe token>",
"ws_url": "wss://api.<env>.appmecca.net/api/ws/deploys",
"expires_in": 60
}
| Status | Cause |
|---|---|
400 |
No deliberate group selected — the channel is per-group, so pass ?group=<slug> for a group you belong to |
401 |
Anonymous |
503 |
Live updates unavailable; clients fall back to polling |
Connect with GET /api/ws/deploys?ticket=<ticket>. The handler re-validates the channel, group and environment bound into the ticket before subscribing.
POST /api/auth/ws-ticket — platform notifications (operators)¶
Mints a ticket for the platform-wide notifications socket. Platform-operator gated — an ordinary tenant gets 403 before any ticket is minted, mirroring GET /api/notifications.
{
"ticket": "<url-safe token>",
"ws_url": "wss://api.<env>.appmecca.net/api/ws/notifications",
"expires_in": 60
}
ws_urlis empty when the API has no domain configured (local dev); the client then derives the URL fromPUBLIC_API_URL.- Returns
503when the live-update backend is unavailable — clients fall back to REST polling.
GET /api/ws/notifications¶
Accepts two credentials:
?ticket=<ticket>— consumed atomically, so a captured ticket is dead after first use and expires after 60 s regardless. The bound payload must carryscope == "admin"and match the API's environment, or the socket closes with code4403.?api_key=<admin key>— the admin API-key path, used by the monitor SPA and admin tooling.
Missing both → close 4401. Invalid/expired/replayed ticket or non-admin → close 4403.
On connect the server sends {"event": "ready"}, then {"event": "ping"} every 30 seconds as keepalive. The protocol is server → client only.