Skip to content

Tenancy

A tenant is one branded deployment: its own name, logo, theme, enabled venues, feature flags, fee schedule, and users. One platform serves many; the API surface is identical for all of them.

Resolution runs before authentication, in this order:

  1. X-Tenant-Slug header — required for server-side integrations. It is trusted only when it names a real tenant; an unknown slug is silently ignored rather than rejected, and resolution falls through. A slug naming a suspended tenant still resolves — it is rejected afterwards with 410 {"error":"tenant_suspended"}, not skipped.
  2. Origin / Host — matched against the tenant’s registered origins. This is how a browser front end on a branded domain resolves without a header. When no Origin is present the Host header is used instead.
  3. Platform default — only when the request carries neither Origin nor Host, or when the origin is the platform tenant’s own registered origin.

Anything else is rejected with 404 {"error":"unknown_tenant_origin"}.

That last point is the wall every integrator hits first. Host is mandatory on every HTTP request, so a server-side call to a shared host like api.troncharts.xyz never falls through to rule 3 — it presents an origin that belongs to no tenant and 404s before your credential is even read.

Every server-side call must send X-Tenant-Slug, or come from an origin registered on your tenant. There is no third option.

Terminal window
curl -s https://api.troncharts.xyz/api/v1/venues \
-H "authorization: Bearer $TOKEN" \
-H "x-tenant-slug: $TC_TENANT_SLUG"

Because an unknown slug is ignored rather than rejected, a typo in the slug gives you the same 404 unknown_tenant_origin as sending no header at all.

A small set of paths skip the gate entirely, because the caller has no tenant to offer yet: the bearer mint and revoke (/api/auth/api-token, /api/auth/api-token/revoke), /api/v1/discovery, self-serve signup (/api/public/tenants/*), the public firm-transparency reads, and the docs themselves. Everything else — including POST /api/auth/bootstrap and all of /api/v1/* — is gated.

X-Tenant-Slug gets you past the origin gate. It does not choose which tenant your credential acts as, and it cannot widen a credential.

A bearer token acts under its credential’s own tenant, read from the credential row on every request; the tenant the request resolved to is deliberately never consulted for scope. So a bearer that reaches a host belonging to another tenant still acts as its own tenant — the request is not rejected, the header is simply not load-bearing for scope. The one case that is rejected (403) is a token whose pinned claim disagrees with its credential row, which happens only if the credential was moved between tenants after the token was minted.

Area Controls
Brand Display name, logo, favicon, support email
Theme Colour tokens, typography, radius — light and dark
Markets Which venues and symbols are enabled
Features Per-tenant feature flags
Fees The tenant’s fee schedule

Read the effective configuration back at any time — useful for mirroring it into your own front end:

Terminal window
curl -s https://api.troncharts.xyz/api/v1/tenants/me \
-H "authorization: Bearer $TOKEN" \
-H "x-tenant-slug: $TC_TENANT_SLUG"
# → { id, displayName, branding, theme, flags, venues, plan, … }

Sub-resources: /api/v1/tenants/{id}/branding, /theme, /flags, /venues, /plan. These are read-only unless your credential carries tenant:config.

Tenant isolation is the load-bearing rule of the platform: every read and every write is filtered by the caller’s tenant, at the query, not in the application layer. This holds for REST and for the WebSocket channels — a socket is scoped to its tenant at handshake and is torn down if that access is revoked mid-connection.

The practical consequence for you: you never have to filter by tenant yourself, and you can treat any row you can see as yours.