Reference
Testing
Test sessions, the update-status override, reading logs and the checklist, and exercising every outcome.
Verification has a lot of branches — approved, declined, review, expired, cancelled, out of credits — and the ones that break in production are the ones nobody exercised. This page covers how to drive each of them, in every language you're likely to be scripting from.
Warning
Use a separate, non-production tenant for all testing. Sessions consume real credits, and any evidence uploaded during testing is real PII in the real store. See Go-live → Sandbox vs. production.
Overriding a session's decision
The fastest way to test your downstream handling is to force a session to a given status with the update-status endpoint. It sets the decision directly and, for terminal statuses, fires the matching webhook.
curl -X PATCH "$BASE/v3/session/$SESSION_ID/update-status/" \
-H "x-api-key: $API_KEY" \
-H "content-type: application/json" \
-d '{ "status": "approved", "reason": "manual test override" }'const res = await fetch(`${BASE}/v3/session/${sessionId}/update-status/`, {
method: "PATCH",
headers: { "x-api-key": API_KEY, "content-type": "application/json" },
body: JSON.stringify({ status: "approved", reason: "manual test override" }),
});
if (!res.ok) throw new Error(`update-status ${res.status}: ${await res.text()}`);import requests
resp = requests.patch(
f"{BASE}/v3/session/{session_id}/update-status/",
headers={"x-api-key": API_KEY},
json={"status": "approved", "reason": "manual test override"},
timeout=15,
)
resp.raise_for_status()status must be a valid session status — not_started, in_progress,
in_review, approved, declined, abandoned, or expired — otherwise you
get 400 with the stable code invalid_status. Drive a session to approved,
then declined, then in_review, and confirm each moves your system to the
right state.
{ "detail": "invalid_status" }Tip
in_review → approved is the sequence most integrations get wrong. Override a
session to in_review (your system should go to a pending state), then override
it to approved, and confirm you move out of limbo on the second webhook,
not the first.
Reading logs and the checklist
Two read endpoints tell you what actually happened inside a session:
GET /v3/session/<id>/logs/ # chronological event timeline
GET /v3/session/<id>/checklist/ # evidence checklist + current stagelogs returns { "session_id", "logs": [{ event, source, message, detail, created_at }] } —
use it to see exactly which module ran, in what order, and why a session was
declined or held:
{
"session_id": "6f0e2b7a-…",
"logs": [
{ "event": "session.created", "source": "api", "message": "Session created", "detail": {}, "created_at": "2026-07-03T08:00:00Z" },
{ "event": "module.id_verification.completed", "source": "engine", "message": "Document authenticated", "detail": { "status": "approved" }, "created_at": "2026-07-03T08:02:11Z" },
{ "event": "module.aml_screening.hit", "source": "engine", "message": "Possible PEP match", "detail": { "risk_level": "medium" }, "created_at": "2026-07-03T08:02:40Z" },
{ "event": "session.routed_to_review", "source": "engine", "message": "AML possible match", "detail": {}, "created_at": "2026-07-03T08:02:41Z" }
]
}checklist returns the evidence checklist plus a stage
field, which is handy for showing progress or debugging a stuck session.
You can also pull the full compact decision at any time:
GET /v3/session/<id>/decision/{
"session_id": "6f0e2b7a-…",
"status": "in_review",
"decision": "in_review",
"decision_v3": { "id_verifications": [{ "status": "approved" }], "liveness_checks": [{ "status": "approved" }], "face_matches": [{ "status": "approved" }], "aml_screenings": [{ "status": "in_review", "risk_level": "medium" }], "reviews": [] },
"reason": ""
}curl -H "x-api-key: $API_KEY" "$BASE/v3/session/$SESSION_ID/logs/"const logs = await fetch(`${BASE}/v3/session/${sessionId}/logs/`, {
headers: { "x-api-key": API_KEY },
}).then((r) => r.json());logs = requests.get(
f"{BASE}/v3/session/{session_id}/logs/",
headers={"x-api-key": API_KEY},
timeout=15,
).json()Exercising the outcomes
The paths worth deliberately testing, in rough order of how often they're skipped:
update-status vs. a real test run: which to use
update-status/ override | A real hosted-flow test run | |
|---|---|---|
| Speed | Instant — one API call | Minutes — capture a document, selfie, etc. |
| What it exercises | Your webhook handler and state machine | The full engine, including module logic and thresholds |
| Costs a credit | No | Yes — SESSION_CREATE plus each module that runs |
| Good for | CI, regression tests, every outcome branch | Pre-launch smoke test, verifying workflow configuration itself |
| Fires the real webhook | Yes, for terminal statuses | Yes |
Tip
Use update-status/ for automated tests that run on every deploy — it's fast,
free of the capture UI, and deterministic. Reserve real test runs (with real or
synthetic test documents) for verifying the workflow configuration itself, e.g.
after changing a liveness method or adding AML.
Webhook development
Point the console's webhook URL at a tunnel (for example ngrok http 3000) and
use Console → Developers → Webhooks → Test to fire a delivery. The
deliveries list shows every attempt, its response code, and a retry button —
which is also the fastest way to test your dedupe logic.
Postman
The repo ships an importable partner-API collection and environment:
docs/postman/obsidian-partner-api.postman_collection.json
docs/postman/obsidian-partner-api.postman_environment.jsonSet base_url and api_key in the environment and the partner surface is one
click away.
Cleaning up
Soft-delete test sessions and their evidence when you're done:
DELETE /v3/session/<id>/delete/ # returns 204curl -X DELETE "$BASE/v3/session/$SESSION_ID/delete/" -H "x-api-key: $API_KEY"await fetch(`${BASE}/v3/session/${sessionId}/delete/`, {
method: "DELETE",
headers: { "x-api-key": API_KEY },
});requests.delete(f"{BASE}/v3/session/{session_id}/delete/", headers={"x-api-key": API_KEY})Troubleshooting
Warning
update-status/ returns 400 with invalid_status. The value must be
one of not_started, in_progress, in_review, approved, declined,
abandoned, expired — check for a typo like "reviewing" or "rejected",
which are not valid session statuses.
Warning
A test override doesn't fire a webhook. Only terminal statuses
(approved, declined, in_review, expired) fire the matching event.
Overriding to not_started or in_progress is a state reset for testing, not
a terminal event, and won't trigger a delivery.
Warning
Test sessions are piling up in the console and skewing dashboards. Delete
them with DELETE /v3/session/<id>/delete/ as part of your test teardown, not
as an afterthought — it also removes the associated test evidence.