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:
{ "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 withpageandper_page. POST /documentsandPOST /live_photos(multipart,typeandsidevalidated),GETby id and byapplicant_id.POST /sdk_tokenfor the Web and mobile SDKs (the SDK itself is not mocked; use workflow runs for the capture page).POST /checkswithreport_names(document,facial_similarity_photo, ...):in_progress, thencompletewithresultclearorconsider.GET /reports?check_id=and/reports/{id}giveresult,sub_result(clear,rejected,suspected,caution),breakdownand extractedproperties.- Onfido Studio:
POST /workflow_runswithworkflow_id(the mock's workflow id credential) returnslink.url, the capture page. Statuses:awaiting_input,processing, thenapproved,declined,revieworabandoned. GET /checks?applicant_id=andGET /checks/{id};GET /workflow_runs/{id}.GET /documents/{id}/downloadand/live_photos/{id}/downloadreturn 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(orid-and-liveness) withexternalUserIdandfixedInfo; a second applicant with the sameexternalUserIdis409.GET /resources/applicants/{id}/oneor/resources/applicants/-;externalUserId={id}/one,.../status,.../requiredIdDocsStatus.POST /resources/applicants/{id}/info/idDoc(multipart:metadataJSON withidDocTypeandcountry, pluscontent), answered withX-Image-Id.POST /resources/applicants/{id}/status/pendingstarts the review (an ID document is required),POST .../resetstarts over.POST /resources/accessTokens/sdk(and the older/resources/accessTokens?userId=) for the WebSDK, andPOST /resources/sdkIntegrations/levels/-/websdkLinkwithlevelNameanduserId, whoseurlis 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/sessionswith{ "verification": { "callback", "person", "document", "vendorData", "endUserId" } }returnsverification.url(the capture page; it redirects tocallbackafterwards) andsessionToken.POST /v1/sessions/{id}/mediawith{ "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: nulluntil 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 /inquirieswithinquiry-template-id(anyitmpl_...),reference-id,fields(name-first,name-last,birthdate) andredirect-uri;GET /inquiries/{id},GET /inquiries?filter[reference-id]=,PATCH,DELETE(redacts).POST /inquiries/{id}/generate-one-time-linkreturnsmeta.one-time-link, the capture page;POST /inquiries/{id}/resumeadds asession-token. After a choice the page redirects toredirect-uri?inquiry-id=<id>.POST /verification/government-ids(inquiry-id,id-class,country-code,front-photoas base64) andPOST /verification/selfies(center-photo), each followed byPOST .../{id}/submit;GET /verifications/{id}. Once both are submitted the inquiry is decided.POST /inquiries/{id}/approve,/declineand/expireact 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(orPUT /api/v1/accounts/{accountId}for a new transaction on an existing account) withcustomerInternalReference,userReference,workflowDefinition.key(10011ID only; other keys add a selfie),callbackUrlandweb.successUrl/web.errorUrl. The answer hasweb.href(the capture page),sdk.tokenand the credentials' API upload URLs.- API channel:
POSTthe files (multipart) to thepartsURLs (FRONT,BACK,FACE), thenPUT /api/v1/accounts/{accountId}/workflow-executions/{id}to finalize. - Retrieval:
GET .../workflow-executions/{id}/status(INITIATED,ACQUIRED,PROCESSED,SESSION_EXPIRED) andGET .../workflow-executions/{id}(also/api/v1/workflow-executions/{id}) withdecisionandcapabilities(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.
POSTto the base URL itself (Shufti's/) with a uniquereference,callback_url,redirect_url,email,countryand thedocumentandfaceservices. Emptyproofvalues make an onsite request:event: request.pendingand averification_url(the capture page). Base64 proofs make an offsite request, answered at once.POST /statusandPOST /deletewith{ "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 VerificationorDocument Verification).POST /verifications/v1/verifywithAcceptTruliooTermsAndConditions: true,ConfigurationName,CountryCode,CustomerReferenceIDandDataFields(PersonInfo,NationalIds,DocumentwithDocumentFrontImageas base64). Person data alone is answered at once withRecord.RecordStatusmatchornomatchandDatasourceResults; documents, or any request withCallBackUrl, are asynchronous:Recordisnulluntil the transaction completes.GET /verifications/v1/transaction/{id}/status(InProgress,Completed) andGET /verifications/v1/transactionrecord/{id}.- Trulioo's capture UI is not part of this API, so the mock adds
POST /testlie/hosted-verificationswithCountryCode,CustomerReferenceIDandReturnUrl, 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 })returnsurl, the capture page, andclient_secret.retrieve(withexpand: ["last_verification_report"]),list,update,cancelandredact;stripe.identity.verificationReports.retrieveandlist.
| 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.