validate_identity_document (Entrust / Onfido)
Business purpose: let a supplier owner verify their identity themselves through Entrust Identity Verification (Onfido) — document capture plus a liveness check — then persist the captured artifacts and the verdict back into Payability.
|
Au10tix / Entrust coexistence. Entrust Identity Verification (Onfido) is the eventual
replacement for Au10tix identity-document validation. During the rollout both vendors run
under the same |
Shape of the flow
Unlike the other processes this is not a two-step ProcessFactory chain:
| Stage | Implementation |
|---|---|
"Request" |
|
Trigger |
|
"Handle" |
|
Rerunning is possible: POST /tasks/execute with
task_name = "entrust/handle_identity_document_validation_response" reprocesses the supplier’s
latest Entrust run (internal callers only) — see api/tasks.adoc#entrust-reprocess.
Session creation
Summarised here; full request/response contract in api/entrust.adoc#create-session.
-
Authorize by source —
INTERNALneedsDEV/FINANCE;SUPPLIER_DASHBOARDandONBOARDINGare self-service. -
Resolve identity — supplier-dashboard callers are pinned to their JWT identity; internal and onboarding callers must pass
supplier_key+person_id. -
Reuse a pending session whose
link_expires_atis still in the future, otherwise enforce the 3-sessions-per-person cap for self-service sources. -
Fetch the owner’s first/last name from the Payability internal API (Onfido requires them).
-
EntrustClient.create_applicantthencreate_workflow_run, tagging the run withcustom_data = {supplier_key}. -
Persist:
TaskExecutionTable.save(task_id=workflow_run_id, …)andToolTable.update(ENTRUST, PENDING). -
Best-effort external syncs → HubSpot
entrust_verification_status = in_progress, plus a supplier event with the same status.
Webhook receiver
interface/web_api/endpoint_handling/handle_entrust_webhook.py:
-
Verify —
X-SHA2-Signaturemust equalHMAC-SHA256(raw_body, ENTRUST_WEBHOOK_TOKEN)in hex, compared withhmac.compare_digest. Missing or mismatched →401. A body that is not valid JSON →400. -
Filter — only
payload.resource_type == "workflow_run"andpayload.action == "workflow_run.completed"are processed; anything else is acknowledged with200 {"status": "ignored"}so Onfido stops retrying. -
Correlate —
payload.object.idis theworkflow_run_id, i.e. theTaskExecutionTabletask_id. The row suppliessupplier_key,person_idandapplicant_id. No row →200 {"status": "ignored"}without scheduling. -
Enqueue —
B2BTaskManager.start_swfwith a deterministic workflow idAutomatic UW Tool - Entrust - {workflow_run_id}, so concurrent redeliveries dedup inside SWF; aCannotRunWorkflow"already started" is treated as already-scheduled and still answered200 {"status": "accepted"}. -
Any unexpected error →
500, which makes Onfido retry the delivery.
{
"payload": {
"resource_type": "workflow_run",
"action": "workflow_run.completed",
"object": {
"id": "b1f7d0c2-3f4e-4a1b-9c8d-2e5f6a7b8c9d",
"status": "approved",
"completed_at_iso8601": "2026-08-12T09:14:03Z",
"href": "https://api.us.onfido.com/v3.6/workflow_runs/b1f7d0c2-3f4e-4a1b-9c8d-2e5f6a7b8c9d"
}
}
}
The aftermath task
task/entrust/handle_identity_document_validation_response/. Input:
{supplier_key, person_id, workflow_run_id, applicant_id?}. correlation_id ==
workflow_run_id.
1. Idempotency guard
If the persisted row is already processed, a redelivered webhook produced a duplicate run:
the task logs and returns the existing output (raw_result_s3_path, status,
document_actions) without touching anything. A missing row is tolerated — processing
continues and the terminal update upserts it.
2. Re-read and archive the run
EntrustClient.get_workflow_run is the source of truth for status (the webhook body carries
only a minimal object). get_workflow_run_tasks then hydrates every step with its input and
output, and the merged document ({…run, tasks: […]}) is archived to:
entrust/identity_document_validation/supplier_key=<sk>/person_id=<pid>/<timestamp>.json
Step hydration is best effort: it fans out into one request per step, and if it fails the run
summary is archived without tasks rather than blocking the verdict.
3. Document handling (required)
Guarded by additional_data.documents_uploaded; skipped entirely when the session recorded no
applicant_id. A failure anywhere in this step fails the task so SWF retries — the raised
error carries the upstream response body so the rejection reason is visible.
-
Archive stale captures —
list_supplier_documents(IDENTIFICATION_DOCUMENT), then a singleupdate_supplier_document_statuses(…, 'archived')over every document whosecompliance.statusis not alreadyarchived(bothpendingandapproved). The fresh capture must fully supersede prior attempts. -
Upload the identity documents + selfie —
get_applicant_files(applicant_id)downloads the Onfido documents and live photos, andupload_supplier_documentsposts them as one multipart request. -
Upload the liveness media —
get_applicant_liveness_files(applicant_id)(live videos and motion captures) in a second request, after the documents.
Both uploads use document_type = IDENTIFICATION_DOCUMENT and the KYC identity
compliance_key, and carry positionally-aligned per-file metadata (one entry per files part,
empty string where absent):
| Field | Value |
|---|---|
|
|
|
The Onfido document type (e.g. |
|
The ISO issuing country — identity documents only. |
|
The run’s |
The ids returned by the uploads land on additional_data.uploaded_document_ids, and a summary
on additional_data.document_actions:
{
"existing_documents_archived": 2,
"id_files_uploaded": 3,
"liveness_files_uploaded": 1,
"uploaded_total": 4,
"approved": 4
}
4. Verdict
Onfido status |
Result |
|---|---|
|
Manual review pending. The |
|
Verdict |
anything else ( |
Verdict |
Onfido gives one overall verdict, so all four identity-document final-status fields
(DocumentAuthenticity, IDNameMatch, IsDocumentExpired, OverallQuality) mirror it —
keeping the existing override schema intact.
On a terminal verdict the task:
-
upserts
FinalStatusTable(idempotent:saveif absent,updateif present), -
sets
ToolTableENTRUST→VERIFIED/NOT_VERIFIED, -
marks the task
processedwith an enrichedoutput(S3 pointer plus the salient run fields:workflow_run_id,applicant_id,workflow_id,workflow_version_id,status,dashboard_url,output,reasons,error,created_at,updated_at,document_actions;Nonevalues are dropped) andadditional_data.full_result_s3_path, -
emits
ProcessFinishedEvent, -
syncs HubSpot and emits the supplier event.
The SWF activity result itself is compact so the SWF console is useful on its own:
{
"raw_result_s3_path": "entrust/identity_document_validation/supplier_key=9b1c.../person_id=44f2.../2026-08-12T09:15:11.204812.json",
"status": "approved",
"verdict": "VERIFIED",
"document_actions": {
"existing_documents_archived": 2,
"id_files_uploaded": 3,
"liveness_files_uploaded": 1,
"uploaded_total": 4,
"approved": 4
}
}
5. Failure path
_update_task_failure_in_task_execution_table marks the task failed and the ENTRUST tool
ERROR, preferring the identity captured at the start of execute() so it still works when
the persisted row is missing. Neither HubSpot nor EventLogger can cause a failure — both clients
swallow their own errors.
External status syncs
Two downstream sinks receive the same lifecycle status. Both are best effort — a failure in either is logged and reported to Sentry but never fails the request or the task:
| Moment | Status value | Sinks |
|---|---|---|
|
|
HubSpot contact property + supplier event |
Run |
|
both; the event also carries |
Run |
|
both; no verdict yet |
Any other terminal status |
|
both; |
In the aftermath task one __sync_external_status call drives both.
See HubSpot and Supplier event logger.
Source files
-
interface/web_api/endpoint_handling/create_entrust_session.py -
interface/web_api/endpoint_handling/handle_entrust_webhook.py -
interface/web_api/router/entrust_router.py -
task/entrust/handle_identity_document_validation_response/ -
components/entrust/client.py,components/hubspot/client.py,components/event_logger/client.py -
Tests:
development/unit_tests/task_/entrust/**,development/unit_tests/components_/entrust/test_client.py,development/unit_tests/components_/test_hubspot_client.py,development/unit_tests/components_/test_supplier_event_logger.py,development/unit_tests/interface_/web_api/endpoint_handling/test_handle_entrust_webhook.py,…/test_create_entrust_session_hubspot.py,…/test_execute_task_entrust_reprocess.py