Concepts
Architecture
How the hosted flow, KYC engine, modules, SDKs and webhooks fit together, and which surfaces you actually call.
ThirdFactor Obsidian is one multi-tenant stack that serves three audiences at once: your applicants (the hosted verification flow), your operators (the console), and you (the partner APIs and webhooks). You integrate against a small, stable surface; almost everything else — workflows, branding, risk lists, credit rates — is configured by an operator in the console, not by API calls from your backend.
This matters for how you plan an integration. You are not writing code against a KYC SDK that runs verification logic on your infrastructure. You are minting short-lived hosted sessions, redirecting or embedding a URL, and reading back a signed result. The verification logic, the document/liveness/AML models and the decision thresholds all run server-side inside Obsidian — nothing sensitive touches your servers except the outcome.
The moving parts
Hosted flow
A branded, per-tenant web flow at /verify/<token> (ThirdFactor Verify) or
/s/<token> (application flows). You mint a URL and redirect the user.
KYC engine
The first-party engine that runs document verification, liveness, face match and AML screening — no external verification SDK required.
SDKs
Web, iOS, Android and Flutter SDKs embed the same hosted flow in your app and surface an advisory result event.
Webhooks
Signed, server-to-server delivery of the authoritative terminal decision.
Each of those four pieces talks to exactly one thing underneath: the KYC
engine's shared model layer. A session created through /v3, a session spawned
by an /api/v1 application, and a session opened through the Web SDK all run
the same workflow engine, the same module scoring and the same credit ledger —
the surface only changes how the session is created and observed, not how
it is scored.
Two API surfaces
Obsidian exposes two REST surfaces on the tenant's own host. They authenticate with the same tenant API key but solve different jobs.
| Surface | Base path | Unit of work | Use it for |
|---|---|---|---|
| ThirdFactor Verify | /v3 | A verification session | Verifying a person: ID, liveness, face match, AML. Fewer moving parts, one webhook. |
| Partner / applications | /api/v1 | An application driven by a flow | A whole onboarding journey: forms, product selection, branching, operator review — with a KYC step inside. |
Note
The /v3 surface has no /api/v1 prefix. Authentication differs slightly:
/v3 takes the key as an x-api-key header; /api/v1/sessions/ takes a
short-lived partner-signed JWT (bearer). See
Authentication.
An application's identity step can spawn a ThirdFactor Verify (/v3) session under the hood,
so completing that KYC session automatically advances the parent application.
One application may therefore own one or more /v3 sessions.
Which surface should you integrate against?
Most integrations pick one surface and stay on it. Use this to decide, and see Sessions for the full lifecycle of each unit of work.
ThirdFactor Verify (/v3) | Partner applications (/api/v1) | |
|---|---|---|
| What you're modelling | "Verify this person" | "Onboard this person" — forms, product choice, identity, decision |
| Auth | x-api-key header | Short-lived signed JWT (bearer) |
| Steps beyond KYC | None — pure verification | Yes — configured as a flow in the console |
| Result shape | decision + decision_v3 module arrays | application.decision + application.status |
| Best for | Standalone identity checks bolted onto an existing product flow | Replacing a multi-step onboarding journey wholesale |
| Typical caller | Backend job, KYC-only feature | Backend that owns the whole signup funnel |
Tip
If you already have your own onboarding forms and product logic, and just need
identity verification as one step, /v3 is almost always the right surface —
it's the smaller contract. Reach for /api/v1 applications when you want
Obsidian to own the whole journey, including branching and operator review
steps that aren't KYC.
Request lifecycle
Create
Your server calls POST /v3/session/ (or POST /api/v1/sessions/) with your
tenant key. This debits the SESSION_CREATE credit and returns a hosted
url.
Redirect or embed
You send the applicant to url, or embed it with an
SDK. The flow runs the workflow's enabled modules; the
server enforces the configured liveness method — it cannot be downgraded
client-side.
Process
The KYC engine scores each module. A session lands in approved, declined,
or in_review (held for an operator).
Notify
On a terminal state, Obsidian sends a signed webhook to your endpoint and
(if set) redirects the applicant to your callback_url.
Confirm
Your server verifies the webhook signature, or reads
GET /v3/session/<id>/decision/, and updates your own record of truth.
The request lifecycle end to end
Your server Obsidian (tenant host) Applicant device
──────────── ───────────────────── ─────────────────
POST /v3/session/ ───────▶ create session, debit credit
resolve workflow_id → graph
◀──────────────────────── { id, url, decision: in_progress }
redirect / embed url
┌──────────────────┐
│ hosted flow walks │
│ the workflow's │
│ enabled nodes │
└──────────────────┘
engine scores each module
session → approved/declined/in_review
◀─────────────────────── signed webhook (terminal state)
redirect to callback_url
GET /v3/session/<id>/decision/ ──▶
◀──────────────────────── authoritative decisionTwo reads confirm the same outcome by design — the webhook is push, the decision endpoint is pull. Use the webhook to react in near real time and the decision endpoint to reconcile anything you might have missed (a webhook delivery failure, a restart mid-processing, a backfill job).
What you talk to, and what you don't
You only ever call two things directly, both over HTTPS on the tenant host:
/v3/*— the ThirdFactor Verify API./api/v1/*— the partner / applications API.
Plus the hosted URLs you hand your users (/verify/<token>, /s/<token>) and
the webhook deliveries you receive. Branding, workflows, flows, risk lists, SMTP
and secrets are all managed by operators in the console — the APIs are
deliberately thin over that configuration.
| Component | Who touches it | How |
|---|---|---|
/v3 and /api/v1 APIs | Your backend | Tenant API key / signed JWT, server-side only |
Hosted flow (/verify, /s) | Your applicant | Browser redirect, iframe embed, or in-app SDK webview |
| Console | Your operators | Username/password + optional 2FA, browser only |
| Webhooks | Your backend | Inbound HTTPS POST, HMAC-signed |
| KYC engine, workflows, risk lists, credit rates | Nobody directly | Configured by operators in the console; you consume the effects (which modules run, what a session costs) through the API |
Multi-tenant isolation
Obsidian resolves the tenant from the host header, and a tenant API key belongs to exactly one tenant. Authenticating is scoping: no partner endpoint takes a tenant identifier, and one tenant can never read or mutate another's sessions, workflows, identities, risk lists or credits. See Tenancy for the full model.
Warning
Whatever a browser or mobile SDK hands back is advisory — it exists so you
can show the right screen. Grant access, open accounts or move money only after
confirming the outcome via the signed webhook or a server-side read of
GET /v3/session/<id>/decision/.
Edge cases worth designing for
- Webhook delivery fails or your endpoint is briefly down. Obsidian retries
signed deliveries; regardless, treat
GET /v3/session/<id>/decision/as the fallback source of truth and poll it for any session you expect a webhook for but haven't received one on within your own timeout. - Your server crashes between "create session" and "store the session ID."
Re-issuing the create call with the same
Idempotency-Key(or the samejtion/api/v1/sessions/) returns the original session instead of minting — and charging for — a second one. See Credits. - The applicant never finishes. The hosted URL simply expires at
expires_at; the session moves toexpired, a terminal state, and emits a webhook like any other terminal state. Mint a fresh session to let them retry. - Two of your backend processes read the same session concurrently. Reads
are safe and side-effect-free; only
update-status/mutates, and it's an explicit operator-grade action, not something concurrent readers trigger.
Common pitfalls
Warning
Trusting the client-side result. The most common integration bug is granting access or unlocking a feature the moment an SDK event fires. SDK/browser results are advisory. Always gate the real action on the signed webhook or a server-side decision read.
Warning
Hard-coding the tenant default workflow. Omitting workflow_id works, but
it silently follows whatever an operator sets as the console default. Pin
workflow_id explicitly once you're in production — see
Workflows.