Connect an app
Inbox has no self-service signup. A consuming app is onboarded through Axis Accounts, which owns identity, apps and workspaces. Inbox only mirrors what Accounts tells it.
The chain has five links. Get them in order — each one fails confusingly if the previous is missing.
1. Register the app in Axis Accounts
The app must exist in Accounts before anything else works. It gets an id and a slug
(axis-engage, axis-social, axis-dashboard, …). Registration happens in Accounts, not here.
Remember what the app is: a caller. axis-social is a consuming app that talks to Inbox; it is not
Inbox itself. See the note on naming in the Overview.
2. Publish Inbox's scope catalogue to that app
Accounts treats app scopes as opaque strings — it stores them and echoes the granted subset back on introspection. The meaning lives in Inbox. A scope must be registered before a key can be issued with it; Accounts' key minting rejects anything that is neither native nor in the target app's registry.
cd apps/service
yarn register-scopes --app axis-engage
This publishes the catalogue defined in src/security/accounts-scopes.ts:
| Accounts scope | Grants (local) | Purpose |
|---|---|---|
inbox:connections |
connections |
Connect, list, disconnect and reconnect accounts |
inbox:threads |
inbox |
Read threads and entries; reply and moderate |
inbox:publish |
publish |
Create, schedule and cancel posts |
inbox:admin |
admin |
Webhook endpoints, webhook inbox, break-glass operations |
inbox:stats |
stats |
Read-only cross-tenant operator figures |
inbox:connections:write |
operator:connections:write |
Cross-tenant operator writes |
Two things about this call:
- It is a full replace.
PUT /api/auth/app-scopes/{appId}replaces the app's catalogue, so the script sends the whole list every time. Never send a partial set. - It needs
app-scopes:writeonINBOX_ACCOUNTS_SERVICE_KEYto publish, and additionallystats:readwhen you pass a slug rather than a UUID — resolving a slug goes throughGET /api/auth/admin/apps. Pass a UUID to skip that lookup and its scope requirement.
The script reads INBOX_ACCOUNTS_BASE_URL and INBOX_ACCOUNTS_SERVICE_KEY, and falls back to
AXIS_ACCOUNTS_APP_ID / INBOX_ACCOUNTS_APP_SLUG when --app is omitted.
3. Mint a service key with inbox:* scopes
Issue an Accounts service key (sk_…) for the app, granting the scopes it actually needs. A key that
only reads and replies to conversations wants inbox:threads; the dashboard's operator view wants
inbox:stats and nothing tenant-scoped.
Grant inbox:admin sparingly. On tenant-scoped routes admin satisfies every scope check. It
deliberately satisfies nothing on the operator surface — cross-tenant reach is a separate, explicit
grant, so an admin key confined to one workspace can never read the whole estate.
Accounts keys are not app-exclusive: one key can carry scopes for several apps. Inbox resolves which app it is acting as from configuration — see the warning in step 5.
4. Choose a tenant value
Every tenant-scoped call carries:
X-Axis-Tenant: ws_<accountsUserGroupId>
The value must be the Accounts user group id, namespaced with ws_. The user group is the workspace;
it is the only identifier shared across Axis apps, so using it is what lets a connection made in one app
appear in another with no reconnect. A per-app local workspace id fragments the same customer across
apps and cannot be un-fragmented later.
There is no validation step here, and that is the trap: an unrecognised tenant value is auto-provisioned as a new empty workspace, not rejected. Read Tenancy before you hardcode anything.
5. Verify with whoami
curl -s \
-H "authorization: Bearer sk_live_…" \
-H "x-axis-tenant: ws_<accountsUserGroupId>" \
https://inbox.example.com/v1/whoami
{
"data": {
"appId": "…",
"mode": "bearer",
"scopes": ["connections", "inbox", "publish"],
"tenantId": "…"
},
"meta": { "requestId": "…" }
}
Check three things: scopes contains what you expect (if a scope is missing, step 2 or 3 is incomplete),
appId is the app you meant, and tenantId is stable across calls with the same header.
A service key may also be presented as x-api-key instead of Authorization: Bearer. Both are accepted.
Use slugs, not app UUIDs
Accounts app ids are environment-specific. axis-engage has a different UUID locally, on dev and in
production. Hardcoding one into configuration that moves between environments produces an app mismatch
that surfaces as an empty workspace or a failing tenant upsert, far from the actual mistake.
The stable key is (tenant slug, app slug). In configuration:
INBOX_ACCOUNTS_APP_SLUG=axis-engage # preferred
AXIS_ACCOUNTS_APP_ID=<uuid> # fallback, environment-specific
INBOX_ACCOUNTS_TENANT_SLUG=<tenant slug>
INBOX_OPERATOR_APP_SLUG=axis-dashboard # a slug, never a UUID
Inbox prefers INBOX_ACCOUNTS_APP_SLUG and falls back to AXIS_ACCOUNTS_APP_ID. If neither is set,
it acts as the first app in the credential's grant — which is not deterministic when a key spans
several apps. Set the slug.
Where to go next
Once whoami returns the scopes and tenant you expect, connect a channel and move a message end to end:
Your first conversation.