API keys & workspaces
Multi-tenant by construction
A workspace is a tenant. Every contact, delivery event, provider account and inbound route is scoped to one — and the scope is resolved from the API key on every request, not from anything the caller claims.
The model
- Workspace — the tenant boundary. Data never crosses it: tool calls, MCP sessions, journal queries and route evaluation all run against the workspace resolved from your credentials.
- API key — a per-workspace credential (
apk_+ 64 hex chars from 32 bytes of CSPRNG entropy). Only its SHA-256 hash is stored; lookups match on the hash and skip revoked keys. - Service key — one shared operator secret, passed as
X-Internal-Keyand compared in constant time. It gates the admin surface (minting keys, creating/listing workspaces) and maps to thedefaultworkspace with full scopes.
How a request authenticates
| Credential | Resolves to | Intended for |
|---|---|---|
Authorization: Bearer apk_… | The key’s workspace + its granted scopes | Tenants — agents and apps |
X-Internal-Key: … | default workspace, full scopes | Operators — admin endpoints, trusted relays |
| None (dev mode) | default workspace, full scopes — only when the API runs without a service key configured | Local development |
curl -X POST "$AGENTPUSH_URL/tools/get_stats" \
-H "Authorization: Bearer apk_…" \
-H "Content-Type: application/json" \
-d '{}'Scopes
| Scope | Grants |
|---|---|
send-only | The default at minting (least privilege) — outbound sending without management or read endpoints. |
read-only | Read surfaces such as the inbox (GET /inbox/senders, GET /inbox/thread) and journal reads. |
full | Everything, including management: provider accounts, webhook registration, inbound routes, audiences. full satisfies any scope check. |
Management endpoints (/provider-accounts, /inbound-routes, …) require full; inbox and journal reads require read-only (or full).
Creating workspaces
POST
/workspacesservice-key gatedGET
/workspacesservice-key ONLY — tenants cannot enumerate tenantscurl -X POST "$AGENTPUSH_URL/workspaces" \
-H "X-Internal-Key: $INTERNAL_SERVICE_KEY" \
-H "Content-Type: application/json" \
-d '{ "name": "Acme Corp", "slug": "acme" }'
// → 201 — the new tenant ("id" may also be supplied explicitly)Listing workspaces is deliberately restricted to the service key: a tenant Bearer key must never enumerate other tenants, and a tenant already knows its own workspace from its key.
Key lifecycle
POST
/admin/api-keysmint — service-key gatedDELETE
/admin/api-keys/:idrevoke — idempotent# mint — raw key returned exactly once
curl -X POST "$AGENTPUSH_URL/admin/api-keys" \
-H "X-Internal-Key: $INTERNAL_SERVICE_KEY" \
-H "Content-Type: application/json" \
-d '{ "workspaceId": "acme", "label": "support-agent", "scopes": ["send-only"] }'
# → { "id": "…", "key": "apk_…", "workspaceId": "acme", "scopes": ["send-only"] }
# revoke — idempotent
curl -X DELETE "$AGENTPUSH_URL/admin/api-keys/$KEY_ID" \
-H "X-Internal-Key: $INTERNAL_SERVICE_KEY"- The raw key appears once in the mint response and is never retrievable again — only the hash is stored.
- Revocation takes effect immediately: revoked keys are excluded from the auth lookup.
- Rotate by minting a new key, deploying it, then revoking the old one — labels (
"support-agent") exist to make this bookkeeping sane.
With a workspace and a key in hand, the whole surface is yours — start at the Quickstart or wire a channel in Channels.