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

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.

SurfaceBase pathUnit of workUse it for
ThirdFactor Verify/v3A verification sessionVerifying a person: ID, liveness, face match, AML. Fewer moving parts, one webhook.
Partner / applications/api/v1An application driven by a flowA 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
Authx-api-key headerShort-lived signed JWT (bearer)
Steps beyond KYCNone — pure verificationYes — configured as a flow in the console
Result shapedecision + decision_v3 module arraysapplication.decision + application.status
Best forStandalone identity checks bolted onto an existing product flowReplacing a multi-step onboarding journey wholesale
Typical callerBackend job, KYC-only featureBackend 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 decision

Two 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.

ComponentWho touches itHow
/v3 and /api/v1 APIsYour backendTenant API key / signed JWT, server-side only
Hosted flow (/verify, /s)Your applicantBrowser redirect, iframe embed, or in-app SDK webview
ConsoleYour operatorsUsername/password + optional 2FA, browser only
WebhooksYour backendInbound HTTPS POST, HMAC-signed
KYC engine, workflows, risk lists, credit ratesNobody directlyConfigured 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 same jti on /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 to expired, 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.

FAQ