API mocks

On this page

Identity verification (KYC)

Mocks compatible with the APIs of identity verification vendors: IDWise, Onfido, Sumsub, Veriff, Persona, Jumio, Shufti Pro, Trulioo and Stripe Identity. Your app starts a verification, the user goes through a hosted page that stands in for the vendor's document and selfie capture, and the mock reports the result through the vendor's own API, status values and signed webhooks. No images are taken or checked: the tester picks the outcome.

No identity is verified. Results are what your tester or scenario chooses, and must never be used for real know-your-customer or anti-money-laundering decisions. Use test applicants and sample documents only: never send real identity documents or selfies (see the acceptable use policy).

Testlie is not affiliated with, endorsed by or sponsored by IDWise, Onfido, Sumsub, Veriff, Persona, Jumio, Shufti Pro, Trulioo or Stripe. Their names are trademarks of their owners, used here only to say which API each mock is compatible with.

The capture page and outcomes

Every KYC mock uses the same hosted capture page. It shows the applicant and the steps the vendor would run (photo ID front and back, selfie with liveness), a Reason field, and one button per outcome:

Button Outcome Meaning
Approve: documents and selfie match Approved Document and selfie pass
Reject: document or face check fails Rejected Verification fails for good (face mismatch, tampered or expired document...)
Ask to resubmit: unreadable photo Resubmit The user must try again (blurry photo, glare, document cut off)
Send to manual review Manual review The vendor flags the case for a human
Leave without finishing Not finished The user abandons the flow; no result is reported

The reason you type is reported as the vendor's decision reason. Results appear after the mock's Asynchronous steps scenario (default 2 seconds): until then the vendor's API shows its in-progress status, and the result webhook is sent when the delay ends. IDWise is the exception: its journeys finish, and its webhook is sent, as soon as the outcome is chosen. Each vendor's guide lists how the outcomes map to its status values.

Verifications submitted through the API alone (no capture page) take their outcome from the scenario: everything is approved or everything is rejected (with the reason you pick). With the default follow the test data, the applicant's name decides: a name containing reject, declin or fraud is rejected, resubmit, retry or blurry asks to resubmit, review or manual goes to manual review, anything else is approved.

The mock's page offers only those three outcome scenarios. To send every API-only verification to manual review, either use test applicants whose name contains review (for example Manual Review), or set the scenario through the REST API: PATCH /api/mocks/:id with { "settings": { "outcome": "require_action" } }. There is no scenario that makes every verification ask to resubmit; use names containing resubmit.

Image and document contents are not stored: the mock keeps only the file's size and a hash (with its name and type, where sent). This applies to multipart uploads and to images sent as base64 in JSON. Document data the vendors extract (name, document number, expiry) is invented from the applicant's name.

IDWise

Create an IDWise mock under API mocks → Identity verification. Use its base URL in place of https://api.idwise.com and its credentials: the API key and secret (Authorization: Basic base64(key:secret)), the flow id (journey definition), your customer id and the webhook bearer token.

Starting a journey

With IDWise the Web or mobile SDK starts the journey in the browser. The mock's stand-in is a Testlie extension:

POST /journey/v2/testlie/journeys
{ "flow_id": "<flow id>", "reference_number": "user-42", "applicant": { "first_name": "Ada", "last_name": "Lovelace" }, "return_url": "https://your.app/kyc/done" }

It returns 201 { journey_id, flow_id, reference_number, url }. Send the user to url, the capture page; after a choice it redirects to return_url?journeyId=<id>. With "auto_complete": true, or any scenario other than follow the test data, the journey finishes at once from the test data.

Results

GET /journey/v2/get/{journey_id} returns the journey result like IDWise's Get Journey Result API: journey_id, customer_id, reference_number, flow_id, system_decision, manual_decision (always null), final_decision, start_time, end_time, is_completed, documents ("0": the photo ID with document_type, issuing_country, extracted_fields such as "Full Name" and "Document Number", and validation_result.validation_checks), selfie (liveness_status, selfie_match) and decision_reasons.

Capture page outcome final_decision
Approved Complete
Manual review Refer
Rejected Rejected
Resubmit, or not finished Incomplete

Errors answer { "detail": "…" }: 401 for bad credentials, 404 for an unknown journey, 400 for an unknown flow id.

Webhook

When a journey finishes, the mock posts IDWise's Finished Journey event to the mock's webhook URL:

json
{ "event": "Finished Journey", "body": { "eventId": "…", "journeyId": "…", "referenceNo": "user-42", "systemDecision": "Complete" } }

with Authorization: Bearer <webhook token>, as IDWise's webhook security settings send it. Check the token, then fetch the result.

Not mocked: IDWise's dashboard review (manual decisions), journey summaries for multi-step flows, AML screening steps and the SDK's in-app events.

Onfido

Create an Onfido mock and use its base URL in place of https://api.eu.onfido.com (the /v3.6 prefix, or any /v3.x, is accepted) with Authorization: Token token=<API token>. The official @onfido/api client works with basePath set to the base URL plus /v3.6.

  • Applicants: POST/GET/PUT/DELETE /applicants, list with page and per_page.
  • POST /documents and POST /live_photos (multipart, type and side validated), GET by id and by applicant_id.
  • POST /sdk_token for the Web and mobile SDKs (the SDK itself is not mocked; use workflow runs for the capture page).
  • POST /checks with report_names (document, facial_similarity_photo, ...): in_progress, then complete with result clear or consider. GET /reports?check_id= and /reports/{id} give result, sub_result (clear, rejected, suspected, caution), breakdown and extracted properties.
  • Onfido Studio: POST /workflow_runs with workflow_id (the mock's workflow id credential) returns link.url, the capture page. Statuses: awaiting_input, processing, then approved, declined, review or abandoned.
  • GET /checks?applicant_id= and GET /checks/{id}; GET /workflow_runs/{id}.
  • GET /documents/{id}/download and /live_photos/{id}/download return a placeholder image: the mock keeps no uploaded images.
  • POST/GET/DELETE /webhooks: webhooks you register receive the same events as the mock's webhook URL.
Capture page outcome Workflow run Check result / document sub_result
Approved approved clear / clear
Rejected declined consider / suspected
Resubmit declined consider / rejected (image integrity)
Manual review review consider / caution

Events: check.started, report.completed, check.completed, workflow_run.completed, as { "payload": { "resource_type", "action", "object": { "id", "status", "href", ... } } } with X-SHA2-Signature: the hex HMAC-SHA256 of the body with the webhook token (WebhookEventVerifier in @onfido/api checks it). Errors use { "error": { "type", "message", "fields" } }.

Sumsub

Use the mock's base URL in place of https://api.sumsub.com. Every request is signed like Sumsub's: X-App-Token, X-App-Access-Ts (Unix seconds, within five minutes) and X-App-Access-Sig, the hex HMAC-SHA256 with the secret key of ts + METHOD + path?query + body (the path from /resources; the full URL path is accepted too).

  • POST /resources/applicants?levelName=basic-kyc-level (or id-and-liveness) with externalUserId and fixedInfo; a second applicant with the same externalUserId is 409.
  • GET /resources/applicants/{id}/one or /resources/applicants/-;externalUserId={id}/one, .../status, .../requiredIdDocsStatus.
  • POST /resources/applicants/{id}/info/idDoc (multipart: metadata JSON with idDocType and country, plus content), answered with X-Image-Id.
  • POST /resources/applicants/{id}/status/pending starts the review (an ID document is required), POST .../reset starts over.
  • POST /resources/accessTokens/sdk (and the older /resources/accessTokens?userId=) for the WebSDK, and POST /resources/sdkIntegrations/levels/-/websdkLink with levelName and userId, whose url is the capture page.
Capture page outcome reviewStatus reviewResult
Approved completed reviewAnswer: GREEN
Rejected completed RED, reviewRejectType: FINAL, rejectLabels
Resubmit completed RED, reviewRejectType: RETRY
Manual review onHold none

Webhooks: applicantCreated, applicantPending, applicantReviewed, applicantOnHold, signed with X-Payload-Digest (hex HMAC-SHA256 of the body with the secret key) and X-Payload-Digest-Alg: HMAC_SHA256_HEX. Errors use { "description", "code", "correlationId", "errorCode", "errorName" }.

Veriff

Use the base URL in place of https://stationapi.veriff.com with X-AUTH-CLIENT: <API key>. Every call after session creation also carries X-HMAC-SIGNATURE: the hex HMAC-SHA256 with the shared secret of the request body, or of the session id for GET and DELETE.

  • POST /v1/sessions with { "verification": { "callback", "person", "document", "vendorData", "endUserId" } } returns verification.url (the capture page; it redirects to callback afterwards) and sessionToken.
  • POST /v1/sessions/{id}/media with { "image": { "context": "document-front", "content": "data:image/jpeg;base64,..." } }, GET /v1/sessions/{id}/media.
  • PATCH /v1/sessions/{id} with { "verification": { "status": "submitted" } } (a document image is required).
  • GET /v1/sessions/{id}/decision (verification: null until decided), /person, /attempts; DELETE /v1/sessions/{id}.
Capture page outcome status code
Approved approved 9001
Rejected declined 9102 (reason code 103)
Resubmit resubmission_requested 9103 (reason code 204)
Manual review review 9121

The mock's webhook URL receives the event webhook (submitted, code 7002; abandoned) and the decision webhook (the decision body), both with X-AUTH-CLIENT and X-HMAC-SIGNATURE over the body. Errors use { "status": "fail", "code", "message" }.

Persona

Use the base URL in place of https://withpersona.com/api/v1 with Authorization: Bearer <API key>. Documents are JSON:API with kebab-case keys; send Key-Inflection: camel for camelCase (request keys may use either).

  • POST /inquiries with inquiry-template-id (any itmpl_...), reference-id, fields (name-first, name-last, birthdate) and redirect-uri; GET /inquiries/{id}, GET /inquiries?filter[reference-id]=, PATCH, DELETE (redacts).
  • POST /inquiries/{id}/generate-one-time-link returns meta.one-time-link, the capture page; POST /inquiries/{id}/resume adds a session-token. After a choice the page redirects to redirect-uri?inquiry-id=<id>.
  • POST /verification/government-ids (inquiry-id, id-class, country-code, front-photo as base64) and POST /verification/selfies (center-photo), each followed by POST .../{id}/submit; GET /verifications/{id}. Once both are submitted the inquiry is decided.
  • POST /inquiries/{id}/approve, /decline and /expire act like the dashboard.
Capture page outcome Inquiry status Verifications
Approved approved passed
Rejected declined failed
Resubmit stays pending (the user can resume) requires_retry
Manual review needs_review passed
Not finished expired

Events (inquiry.created, inquiry.completed, inquiry.approved, inquiry.declined, inquiry.marked-for-review, inquiry.expired, verification.created, verification.submitted, verification.passed, verification.failed, verification.requires-retry) are sent as { "data": { "type": "event", "attributes": { "name", "payload" } } } with Persona-Signature: t=<unix>,v1=<hex HMAC-SHA256 of "<t>.<body>"> and the webhook secret. Errors use { "errors": [{ "title", "details" }] }.

Jumio

Use the base URL for all of Jumio's hosts (auth, account and retrieval). Get a token with POST /oauth2/token (grant_type=client_credentials, Authorization: Basic base64(client_id:client_secret)), then call with Authorization: Bearer <token> (valid one hour).

  • POST /api/v1/accounts (or PUT /api/v1/accounts/{accountId} for a new transaction on an existing account) with customerInternalReference, userReference, workflowDefinition.key (10011 ID only; other keys add a selfie), callbackUrl and web.successUrl / web.errorUrl. The answer has web.href (the capture page), sdk.token and the credentials' API upload URLs.
  • API channel: POST the files (multipart) to the parts URLs (FRONT, BACK, FACE), then PUT /api/v1/accounts/{accountId}/workflow-executions/{id} to finalize.
  • Retrieval: GET .../workflow-executions/{id}/status (INITIATED, ACQUIRED, PROCESSED, SESSION_EXPIRED) and GET .../workflow-executions/{id} (also /api/v1/workflow-executions/{id}) with decision and capabilities (usability, extraction, imageChecks, dataChecks, liveness, similarity).
Capture page outcome decision.type (risk score) Capabilities
Approved PASSED (0) all PASSED
Rejected REJECTED (100) imageChecks REJECTED with your reason as the label (default MANIPULATED_DOCUMENT)
Resubmit REJECTED (80) usability REJECTED BAD_QUALITY
Manual review WARNING (50) similarity WARNING NOT_POSSIBLE
Not finished NOT_EXECUTED (-1) status SESSION_EXPIRED

The callback (callbackSentAt, userReference, workflowExecution { id, href, definitionKey, status }, account) goes to callbackUrl, or the mock's webhook URL when none was given. Jumio does not sign callbacks (it publishes callback IP addresses), so the mock sends them unsigned: fetch the result from the retrieval API.

Shufti Pro

Use the base URL in place of https://api.shuftipro.com with Authorization: Basic base64(client_id:secret_key). Responses (including validation errors) and callbacks carry Shufti's Signature header, sha256hex(rawBody + sha256hex(secretKey)): the hex SHA-256 of the raw body with the hex SHA-256 of the secret key appended. Check it before trusting the body. Authentication errors (401) and the errors forced by the API failure scenario are not signed.

  • POST to the base URL itself (Shufti's /) with a unique reference, callback_url, redirect_url, email, country and the document and face services. Empty proof values make an onsite request: event: request.pending and a verification_url (the capture page). Base64 proofs make an offsite request, answered at once.
  • POST /status and POST /delete with { "reference" }.
Capture page outcome event verification_result
Approved verification.accepted all 1
Rejected verification.declined face: 0, declined_reason
Resubmit verification.declined document checks 0, declined_reason
Manual review review.pending
Not finished verification.cancelled

Callbacks go to callback_url (or the mock's webhook URL) with the same body as /status; POST /delete also sends a request.deleted callback. Errors use { "reference", "event": "request.invalid", "error": { "service", "key", "message" } }; only the document and face services are mocked.

Trulioo

Use the base URL in place of https://gateway.trulioo.com/trial (the /trial prefix is optional) with x-trulioo-api-key: <API key> (Basic auth with the API username and key works too).

  • GET /connection/v1/testauthentication, /connection/v1/sayhello/{name} and /configuration/v1/countrycodes/{configuration} (Identity Verification or Document Verification).
  • POST /verifications/v1/verify with AcceptTruliooTermsAndConditions: true, ConfigurationName, CountryCode, CustomerReferenceID and DataFields (PersonInfo, NationalIds, Document with DocumentFrontImage as base64). Person data alone is answered at once with Record.RecordStatus match or nomatch and DatasourceResults; documents, or any request with CallBackUrl, are asynchronous: Record is null until the transaction completes.
  • GET /verifications/v1/transaction/{id}/status (InProgress, Completed) and GET /verifications/v1/transactionrecord/{id}.
  • Trulioo's capture UI is not part of this API, so the mock adds POST /testlie/hosted-verifications with CountryCode, CustomerReferenceID and ReturnUrl, returning { "TransactionID", "Url" }.
Capture page outcome RecordStatus Document Verification datasource
Approved match Validated and FaceMatch match
Rejected nomatch Validated nomatch, AuthenticityReasons
Resubmit nomatch error 3100
Manual review match FaceMatch nomatch, Rule.Note "Manual review required"

The completion callback (TransactionId, TransactionRecordId, Status: Completed) goes to CallBackUrl or the mock's webhook URL, unsigned like Trulioo's. Errors use { "Message" } or { "Errors": [{ "Code", "Message" }] }.

Stripe Identity

Stripe Identity is part of the Stripe mock: use the same mock, key and webhook secret, and the official Stripe SDKs.

  • stripe.identity.verificationSessions.create({ type: "document", options: { document: { require_matching_selfie: true } }, return_url, client_reference_id }) returns url, the capture page, and client_secret.
  • retrieve (with expand: ["last_verification_report"]), list, update, cancel and redact; stripe.identity.verificationReports.retrieve and list.
Capture page outcome Session status Details
Approved processing, then verified verified_outputs, report document.status: verified
Rejected processing, then requires_input last_error.code: selfie_face_mismatch
Resubmit processing, then requires_input last_error.code: document_unverified_other
Manual review stays processing Stripe Identity has no review outcome
Not finished requires_input last_error.code: abandoned

After requires_input the same url takes another attempt. Events: identity.verification_session.created, .processing, .verified, .requires_input, .canceled and .redacted, signed with Stripe-Signature like every Stripe mock event.

Not mocked for these vendors: their browser and mobile SDKs (use the capture page links), AML and watchlist screening, address and phone verification, and dashboard case management beyond the actions listed above. Uqudo is not mocked yet.