Concepts

Tenancy

Multi-tenant isolation — host-based tenant resolution, tenant-scoped API keys, per-tenant data and configuration.

Obsidian is multi-tenant by construction. A tenant is resolved from the host header, and every API key belongs to exactly one tenant. You never send a tenant identifier: authenticating is scoping.

This is a deliberate design choice, not an implementation detail you can route around. There is no tenant_id field anywhere on the partner API surface — not on session create, not as a query parameter, not in a JWT claim you set yourself. The host you call and the key you send are the only two things that determine whose data you're touching, which means tenant isolation can't be defeated by a client-side bug that forgets to filter.

Host-based resolution

Each tenant is served on its own hosts.

HostServes
acme.example.comThe customer-facing flow for tenant acme — onboarding flows, hosted KYC (/verify/<token>, /s/<token>), the /v3 and /api/v1 APIs.
acme-console.example.comThe operator console for acme.

The request host determines the tenant, so the hosted URL you hand an applicant is always on the tenant's own domain, and the API base you call is on that host too.

                    Host header               Resolves to
                    ──────────────             ───────────
  acme.example.com          ──────────────▶     tenant "acme"
                                                  ├─ /v3/*            (ThirdFactor Verify API)
                                                  ├─ /api/v1/*        (partner API)
                                                  ├─ /verify/<token>  (hosted ThirdFactor Verify)
                                                  └─ /s/<token>       (hosted application flow)

  acme-console.example.com  ──────────────▶     tenant "acme"
                                                  └─ operator console (Config, Verifications, Identities…)

  globex.example.com        ──────────────▶     tenant "globex"      (fully separate data, keys, config)

Note

Because resolution is host-based, the same relative path (/v3/session/, /verify/<token>) means a different tenant's data depending purely on which host you hit it on. There is no cross-host fallback — a key issued for acme.example.com does not work against globex.example.com.

API keys are the tenant

A tenant API key belongs to exactly one tenant. That is why no partner endpoint takes a tenant identifier — the key resolves it. Keys are created in Console → Settings → API keys and sent as:

  • x-api-key: <key> on the ThirdFactor Verify API (/v3).
  • Authorization: Bearer <key> on the partner API (/api/v1), or wrapped in a short-lived signed JWT for POST /api/v1/sessions/.

See Authentication for the key and JWT details.

Warning

API keys are secret and act as the whole tenant. Use them only from your server. A leaked key lets anyone create sessions and read your verifications.

Data isolation

Resources are never shared across tenants. Sessions, applications, workflows, identities, enrolled faces and signatures, risk lists, webhooks, credits and config are all per-tenant.

ResourceIsolation
Sessions & applicationsNever visible or mutable across tenants; a session created under one key cannot be read with another.
WorkflowsEach tenant configures and owns its own set; workflow_id values are meaningless outside the tenant that created them.
Identities & enrolled faces1:N face search and AML re-screening only ever run against your tenant's own enrolled population.
Risk listsPer-tenant watchlists and allow/deny entries — one tenant's list never affects another's screening.
WebhooksConfigured (URL + secret) per tenant; deliveries are scoped to that tenant's own session/application events.
CreditsBalance and per-feature rates are per-tenant; spend on one tenant never draws down another's balance.
Branding & flow copyLogo, colours, languages and hosted-flow content are per-tenant — applicants of different tenants never see each other's branding.

Consequences worth designing around:

  • A 404 on another tenant's session is indistinguishable from a 404 on a session that doesn't exist. That is intentional — one tenant cannot probe another's data.
  • A 1:N face search or the rolling AML re-screen only ever covers your enrolled population.
  • If you run several brands or legal entities on one deployment, they are separate tenants with separate keys, hosts and data.

Per-tenant configuration

Almost everything you might expect to configure by API is instead configured by an operator in the console, per tenant:

Verification

KYC workflows and the default workflow, liveness methods, forgery actions, flows, forms and products.

Branding & content

Logo, colours, languages and flow copy for the hosted flow.

Delivery & secrets

SMTP settings, the webhook secret, JWT signing secrets, and hosted-session expiry defaults.

Risk & billing

Risk lists, per-module credit rates and the credit balance.

The APIs are deliberately thin over this configuration. Your integration mints sessions and reads results; operators own the policy.

Note

Because config is per-tenant, the same POST /v3/session/ call can behave differently for two tenants — different default workflow, different enabled modules, different branding. Pin workflow_id explicitly if you need identical behaviour across tenants you operate. See Workflows.

Operating multiple tenants

If your business runs more than one brand, region, or legal entity through Obsidian, each is a genuinely separate tenant — not a flag or a scoped sub-resource of one tenant. That means:

  • Each needs its own host, its own API key(s), and its own console login.
  • Workflows, branding, risk lists and credit balances are configured independently for each — nothing is inherited from a "parent" tenant.
  • Your backend integration code can be shared (the API shape is identical), but your configuration — which BASE you call, which key you send — must be looked up per tenant/brand rather than hard-coded once.

Tip

If you operate several tenants, keep a small internal mapping of brand → { base_url, api_key } in your own config rather than assuming one global BASE/TF_API_KEY pair. It's the same pattern as multi-region API credentials for any other vendor.

Edge cases

  • Reusing a key across environments. A key issued for one tenant's host will simply fail authentication (not silently work) if sent to a different tenant's host — there's no implicit fallback or shared default tenant.
  • A session ID that looks valid but returns 404. This is the expected shape of "wrong tenant," not a corrupted ID — re-check which key/host you used to create the session in the first place.
  • Webhook secrets are per-tenant. If you operate multiple tenants, each has its own signing secret; verifying a payload from tenant B with tenant A's secret always fails signature verification — see Webhooks.

Warning

Common pitfall: assuming a workflow_id, session ID, or identity ID you captured under one tenant's key will resolve under another. IDs are not globally unique in a way you can rely on across tenants — always pair an ID with the tenant (host + key) it was created under.

FAQ