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-Key and compared in constant time. It gates the admin surface (minting keys, creating/listing workspaces) and maps to the default workspace with full scopes.

How a request authenticates

CredentialResolves toIntended for
Authorization: Bearer apk_…The key’s workspace + its granted scopesTenants — agents and apps
X-Internal-Key: …default workspace, full scopesOperators — admin endpoints, trusted relays
None (dev mode)default workspace, full scopes — only when the API runs without a service key configuredLocal development
authenticated call
curl -X POST "$AGENTPUSH_URL/tools/get_stats" \
  -H "Authorization: Bearer apk_…" \
  -H "Content-Type: application/json" \
  -d '{}'

Scopes

ScopeGrants
send-onlyThe default at minting (least privilege) — outbound sending without management or read endpoints.
read-onlyRead surfaces such as the inbox (GET /inbox/senders, GET /inbox/thread) and journal reads.
fullEverything, 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 gated
GET/workspacesservice-key ONLY — tenants cannot enumerate tenants
create a tenant
curl -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 gated
DELETE/admin/api-keys/:idrevoke — idempotent
mint and revoke
# 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.