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:

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.