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.
| Host | Serves |
|---|---|
acme.example.com | The customer-facing flow for tenant acme — onboarding flows, hosted KYC (/verify/<token>, /s/<token>), the /v3 and /api/v1 APIs. |
acme-console.example.com | The 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 forPOST /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.
| Resource | Isolation |
|---|---|
| Sessions & applications | Never visible or mutable across tenants; a session created under one key cannot be read with another. |
| Workflows | Each tenant configures and owns its own set; workflow_id values are meaningless outside the tenant that created them. |
| Identities & enrolled faces | 1:N face search and AML re-screening only ever run against your tenant's own enrolled population. |
| Risk lists | Per-tenant watchlists and allow/deny entries — one tenant's list never affects another's screening. |
| Webhooks | Configured (URL + secret) per tenant; deliveries are scoped to that tenant's own session/application events. |
| Credits | Balance and per-feature rates are per-tenant; spend on one tenant never draws down another's balance. |
| Branding & flow copy | Logo, colours, languages and hosted-flow content are per-tenant — applicants of different tenants never see each other's branding. |
Consequences worth designing around:
- A
404on another tenant's session is indistinguishable from a404on 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
BASEyou 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.