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 stage

logs 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/ overrideA real hosted-flow test run
SpeedInstant — one API callMinutes — capture a document, selfie, etc.
What it exercisesYour webhook handler and state machineThe full engine, including module logic and thresholds
Costs a creditNoYes — SESSION_CREATE plus each module that runs
Good forCI, regression tests, every outcome branchPre-launch smoke test, verifying workflow configuration itself
Fires the real webhookYes, for terminal statusesYes

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

Set 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 204
curl -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.

FAQ