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.

If you authenticate with an API credential, there is nothing for you to do here. Your credential is bound to one tenant when it is minted, and that binding is what scopes every call — the server reads it off your credential row on every request. No header, Origin or Host can widen it, narrow it, or contradict it.

Terminal window
# This is the whole thing. No tenant header.
curl -s https://api.troncharts.xyz/api/v1/venues \
-H "authorization: Bearer $TOKEN"

Calling from a host that belongs to no tenant — api.troncharts.xyz, your own backend, a Lambda — is the normal case and is accepted. That was not always true: until 2026-08-17 such a call was refused with 404 {"error":"unknown_tenant_origin"} before your credential was read, which forced every integration to send X-Tenant-Slug naming a tenant its key already named. If you have that header wired up, you can leave it or drop it; it changes nothing.

With no credential there is nothing to read the tenant from, so a browser session resolves in this order:

  1. X-Tenant-Slug header — 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 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"}. The same 404 is what a credential-less or invalid-credential server call still gets: an expired, revoked or malformed bearer leaves nothing to pin the tenant to, so the rejection lands exactly where it did before.

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.

X-Tenant-Slug decides the tenant for cookie callers only. 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. 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 inert. The one case that is rejected (403 tenant_mismatch) 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. Re-mint and it clears.

A suspended tenant returns 410 {"error":"tenant_suspended"}, checked against your credential’s tenant — reaching a live tenant’s host does not revive a suspended key.

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.