Entrust endpoints

Two surfaces on entrust_router: an authenticated session endpoint and an unauthenticated, signature-verified webhook. The business flow behind them is in validate_identity_document (Entrust / Onfido).

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 validate_identity_document process, so the GSI, dedup logic and admin reporting keep working unchanged. ToolType.get_by_process_name therefore still maps validate_identity_document to AU10TIX; the Entrust code paths reference ToolType.ENTRUST directly. Do not switch that mapping until Au10tix is fully retired.

POST /entrust/sessions

Creates (or reuses) an Entrust Identity Verification session for a supplier owner and returns the hosted verification URL.

Request body — CreateEntrustSessionRequest; both fields are optional at the HTTP layer because the requirement depends on the caller:

{ "supplier_key": "9b1c4d3e-6f2a-4b8c-9d0e-1f2a3b4c5d6e", "person_id": "44f2a1b0-7c3d-4e5f-8a9b-0c1d2e3f4a5b" }
Source Identity resolution

supplier_dashboard

Self-service only. The body may be omitted entirely — supplier_key comes from the token’s supplier_key and person_id from its login_key. If a supplier_key is sent it must match the token, else 400.

internal

Acts on someone else’s behalf, so supplier_key and person_id are both required (400 otherwise). Needs DEV, FINANCE or INT_SUPER_USER.

onboarding

Self-service origin used by the supplier onboarding flow; like internal it must supply both fields, but it needs no permissions.

Behaviour:

  • Prior-run lookup — reads the supplier’s validate_identity_document executions from the GSI and keeps only Entrust rows (those whose input carries an applicant_id) for this person_id, so historical Au10tix runs never affect dedup or rate limiting.

  • Dedup — if one of those rows is pending and its additional_data.hosted_link is still valid (link_expires_at in the future, or absent), that URL, workflow_run_id and sdk_token are returned instead of minting a new run. An unparseable expiry is treated as stale.

  • Rate limit — SUPPLIER_DASHBOARD and ONBOARDING are capped at 3 sessions per person → 429. INTERNAL is unlimited.

  • Name lookup — PayabilityInternalAPI.get_supplier_info(detailed=True), then the owner whose loginKey == person_id. Middle names are stripped from firstName. Unknown supplier (400/404 upstream) → 404; missing owner or missing name → 404; other upstream failures or connection errors → 502.

  • Session creation — create_applicant then create_workflow_run against ENTRUST_WORKFLOW_ID, tagged with custom_data = {"supplier_key": …}. A 4xx from Onfido is passed through with its status and the parsed upstream body under entrust_error; a 5xx or connection error becomes 502.

  • Persistence — TaskExecutionTable.save(task_id=workflow_run_id, …, status=pending) with input = {supplier_key, person_id, applicant_id, workflow_run_id} and additional_data = {hosted_link, link_expires_at, sdk_token}, then ToolTable.update(ENTRUST, PENDING, correlation_id=workflow_run_id).

  • Downstream syncs — HubSpot entrust_verification_status = in_progress and a supplier event (status = in_progress, tool = ENTRUST) to the internal EventLogger. Both are best effort; neither can fail the request. See HubSpot and Supplier event logger.

Response — EntrustSessionResponse:

{
  "url": "https://alt.onfido.app/l/eyJhbGciOi...",
  "workflow_run_id": "b1f7d0c2-3f4e-4a1b-9c8d-2e5f6a7b8c9d",
  "sdk_token": "eyJhbGciOiJFUzUxMiJ9...",
  "link_expires_at": "2026-08-19T09:14:03Z"
}

A metric create_entrust_session_endpoint_time_taken is emitted on every call, success or failure.

Systems touched: Payability internal API, Entrust/Onfido, TaskExecutionTable, ToolTable, ChangelogTable, HubSpot, EventLogger.

POST /entrust/webhook

Receives Onfido workflow-run completion callbacks. Unauthenticated by design — Onfido has no Payability JWT — so it lives on a separate webhook_router without the JWTAuthorizer dependency and establishes trust by signature instead.

The handler is deliberately thin: verify, correlate, enqueue. All slow work (Onfido re-read, S3 archive, document download and re-upload, verdict) happens in the SWF task entrust/handle_identity_document_validation_response, mirroring how the Au10tix request task schedules its response task.

Check Outcome

X-SHA2-Signature == HMAC-SHA256(raw_body, ENTRUST_WEBHOOK_TOKEN) (hex, compare_digest)

Missing header, unset secret or mismatch → 401 {"error_message": "Invalid signature"}

Body parses as JSON

Otherwise 400 {"error_message": "Malformed JSON body"}

payload.resource_type == "workflow_run" and payload.action == "workflow_run.completed"

Anything else → 200 {"status": "ignored"} so Onfido stops retrying

payload.object.id present

Missing → 200 {"status": "ignored"}

TaskExecutionTable.get(workflow_run_id) exists

Missing row (session never minted or purged) → 200 {"status": "ignored"}

B2BTaskManager.start_swf succeeds

200 {"status": "accepted"}. CannotRunWorkflow (a concurrent delivery already scheduled it) is also 200 accepted. Any other exception → 500 so Onfido retries.

The scheduled activity carries task_input = {supplier_key, person_id, applicant_id, workflow_run_id} and task_correlation_id = workflow_run_id, with the deterministic workflow id Automatic UW Tool - Entrust - {workflow_run_id}.

The raw request body must be read untouched (await request.body()) because the HMAC is computed over the exact received bytes.

Systems touched: TaskExecutionTable (read only), SWF via the B2B Task Manager.

Configuring the webhook in Onfido

  1. Point the Onfido webhook at POST https://<host>/entrust/webhook (dev hosts include the /automatic-uw-tool-web-api root path).

  2. Subscribe to the workflow_run.completed event.

  3. Use the same shared secret that is stored as ENTRUST_WEBHOOK_TOKEN in the automatic_uw_tool secret — see Settings & secrets reference.

  4. The workflow itself is referenced by ENTRUST_WORKFLOW_ID; changing the Studio workflow means changing that setting in settings_setup.py.