Get started
Quickstart
Go from a sandbox key to a verified result in about five minutes. You will create an onboarding request, open the link, complete the flow with test data, receive a signed webhook and fetch the result.
What you need
A VerifyMe workspace (free sandbox), a terminal with curl, and optionally a public HTTPS URL for webhooks. In this guide https://kyc.example.com stands for your VerifyMe deployment host. Use the one shown in your portal; you can type it into Your VerifyMe host in the sidebar and every sample on every docs page will update.
Get a sandbox API key
- Sign in to the portal and open Developers, API keys.
- Choose Create key, name it, keep the default scopes (
onboarding.create,onboarding.read,onboarding.result,onboarding.cancel) and addwebhook.manageif you want to manage endpoints through the API. Confirm with your password (step-up). - Copy the key now. It is shown once and only a hash is stored.
Sandbox keys start with
vm_test_. Export it so the samples can use it:export VERIFYME_API_KEY="vm_test_xxxxxxxx_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" export VERIFYME_HOST="https://kyc.example.com" # Sanity check: should print your tenant and scopes curl -s "$VERIFYME_HOST/v1/ping" -H "Authorization: Bearer $VERIFYME_API_KEY"{ "ok": true, "environment": "test", "tenantId": "tnt_01M3TRRAAAS88X5T0X1F", "scopes": ["onboarding.create", "onboarding.read", "onboarding.result", "onboarding.cancel"] }Keys are bound to an environment
A
vm_live_key sent to the sandbox, or avm_test_key sent to production, returns401 VERIFYME_UNAUTHENTICATED. See Authentication.Create an onboarding request
curl -X POST "$VERIFYME_HOST/v1/onboarding-requests" \ -H "Authorization: Bearer $VERIFYME_API_KEY" \ -H "Idempotency-Key: order-1001-kyc" \ -H "Content-Type: application/json" \ -d '{ "externalReference": "ORD-1001", "applicantType": "merchant", "mobile": "9876541234", "email": "ravi@example.com", "returnUrl": "https://shop.example.com/kyc/done", "expiresInHours": 48, "metadata": { "plan": "gold" } }'const res = await fetch(`${process.env.VERIFYME_HOST}/v1/onboarding-requests`, { method: "POST", headers: { Authorization: `Bearer ${process.env.VERIFYME_API_KEY}`, "Idempotency-Key": "order-1001-kyc", "Content-Type": "application/json", }, body: JSON.stringify({ externalReference: "ORD-1001", applicantType: "merchant", mobile: "9876541234", email: "ravi@example.com", returnUrl: "https://shop.example.com/kyc/done", expiresInHours: 48, metadata: { plan: "gold" }, }), }); if (!res.ok) throw new Error((await res.json()).error.message); const request = await res.json(); console.log(request.onboardingUrl);import os, requests r = requests.post( f"{os.environ['VERIFYME_HOST']}/v1/onboarding-requests", headers={ "Authorization": f"Bearer {os.environ['VERIFYME_API_KEY']}", "Idempotency-Key": "order-1001-kyc", }, json={ "externalReference": "ORD-1001", "applicantType": "merchant", "mobile": "9876541234", "email": "ravi@example.com", "returnUrl": "https://shop.example.com/kyc/done", "expiresInHours": 48, "metadata": {"plan": "gold"}, }, timeout=15, ) r.raise_for_status() print(r.json()["onboardingUrl"]){ "requestId": "vm_req_01M3TRRXRBKXXX2JCEEX", "externalReference": "ORD-1001", "status": "LINK_ISSUED", "onboardingUrl": "https://kyc.example.com/start/vm_t_XWRW-DmQVxZwTXYHcqQzODYAgtyf8Z-tF0tMI9HfCNQ", "expiresAt": "2026-10-03T03:40:18.571Z" }Store
requestIdagainst your own record. If you pass amobileoremail, the applicant does not have to type it and it is locked in the OTP step.Idempotency
Re-sending the same
Idempotency-Keywith the same body returns the original response and adds the headerIdempotent-Replayed: true. The same key with a different body returns409 IDEMPOTENCY_KEY_REUSED. Keys are kept for 24 hours.Open the onboarding URL
Paste
onboardingUrlinto a browser. You land on the VerifyMe hosted experience, branded with your tenant logo and colours. In production you would redirect the user, or open the link in the embedded Web SDK or a mobile WebView (see the integration guide).The link is a one-time capability: the token is exchanged for a short-lived applicant session. Anyone holding the link can complete the request until it expires, so treat it like a password reset link and send it only to the applicant.
Complete the flow in the sandbox
The default flow is India Business KYC. In the sandbox every provider is simulated, and the values you type decide the outcome. The OTP is returned on screen as a sandbox code (and appears in the portal's sandbox inbox), so you never need a real phone.
Step Enter Result Mobile OTP Any valid mobile (10 digits starting 6 to 9). Mobile ending 1234OTP is always 123456. Other numbers get a random code, still shown on screenMobile OTP Mobile ending 0000Simulated SMS gateway timeout. Delivery is queued and the step shows a delayed state Email OTP Email starting fail@Simulated SMTP outage DigiLocker (Aadhaar) Click Allow access on the simulator page Completes with name TEST APPLICANT, date of birth1990-05-14, Bengaluru address and a random Aadhaar last four. Deny produces a retryable failurePAN Valid format, for example ABCPK1234FVerified, holder name matches PAN Ends with X, for exampleABCPK1234XPAN_INVALID(registry says not found)PAN 5th character T, for exampleABCPT1234FSimulated registry outage: PROVIDER_UNAVAILABLE, attempt not consumed, never a false rejectionPAN 5th character Z, for exampleABCPZ1234FVerified but holder is DIFFERENT PERSON: flagPAN_AADHAAR_MISMATCH(orPAN_NAME_MISMATCHwithout DigiLocker)Bank Account 9 to 18 digits and IFSC such as HDFC0001234Verified. Bank name comes from the IFSC prefix ( HDFC,SBIN,ICIC)Bank Account ending 9999Verified with holder SOME OTHER HOLDER: flagBANK_NAME_MISMATCHBank Account starting 0000BANK_ACCOUNT_INVALIDBank Account ending 1111Simulated gateway timeout: PROVIDER_UNAVAILABLESelfie JPEG, PNG or WebP of at least 1,500 bytes Face match score 72 to 98, passes the default threshold of 60 Selfie Image smaller than 1,500 bytes Score 38: FACE_MISMATCH, retry; after the attempt limit the step completes with flagFACE_BELOW_THRESHOLDfor reviewMore triggers (liveness, uploads, pincode, malware scan) are in Sandbox and testing.
Receive the webhook
VerifyMe sends a signed
POSTto your endpoint for every event. For a quick start use the sandbox sink, a built-in receiver that verifies the signature exactly like your server should and lists the last 30 deliveries in the portal.- Portal, Developers, Webhooks, choose Create sandbox sink (or
POST /api/portal/webhooks/sandbox-sink). It subscribes to all events athttps://kyc.example.com/demo/sink/<endpointId>. - Or register your own HTTPS endpoint through the API:
curl -X POST "$VERIFYME_HOST/v1/webhook-endpoints" \ -H "Authorization: Bearer $VERIFYME_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "url": "https://shop.example.com/hooks/verifyme", "events": ["onboarding.submitted", "onboarding.approved", "onboarding.rejected"] }'The response contains the signing
secret(whsec_...) once. Each delivery looks like this:POST /hooks/verifyme HTTP/1.1 Content-Type: application/json User-Agent: VerifyMe-Webhooks/1.0 X-VerifyMe-Event-Id: evt_01M3TRSBZ95445F69Z4C X-VerifyMe-Timestamp: 1790826033 X-VerifyMe-Signature: v1=5f1c0e9b4d7a...e2 X-VerifyMe-Delivery: del_01M3TRSBZB1QX4GK6C0V X-VerifyMe-Attempt: 1{ "eventId": "evt_01M3TRSBZ95445F69Z4C", "eventType": "onboarding.submitted", "schemaVersion": "1", "tenantId": "tnt_01M3TRRAAAS88X5T0X1F", "requestId": "vm_req_01M3TRSBVVT2WJ6MNSSS", "externalReference": "ORD-2001", "occurredAt": "2026-10-01T03:40:33.129Z", "environment": "test", "data": { "status": "SUBMITTED", "resubmission": false } }Respond with any
2xxwithin 8 seconds. Always verify the signature before trusting the body: see Webhooks.- Portal, Developers, Webhooks, choose Create sandbox sink (or
Fetch the result
After the applicant submits, read the result with the
onboarding.resultscope. Never trust the query string on your return URL; always read the result from the API or the webhook.curl -s "$VERIFYME_HOST/v1/onboarding-requests/vm_req_01M3TRSBZTTWMHGGTTGP/result" \ -H "Authorization: Bearer $VERIFYME_API_KEY"{ "requestId": "vm_req_01M3TRSBZTTWMHGGTTGP", "externalReference": "EMP-77", "status": "SUBMITTED", "decision": null, "verified": { "fullName": "TEST APPLICANT", "dateOfBirth": "1990-05-14", "gender": "M", "address": { "line": "12, MG Road", "city": "Bengaluru", "state": "Karnataka", "pincode": "560001" }, "mobile": "98XXXXXX34", "email": null, "pan": "ABXXXXXX4F", "aadhaar": "XXXX XXXX 3354", "bankAccount": null, "bankAccountHolder": null, "faceMatchScore": 78, "faceMatchPassed": true, "livenessPassed": null }, "flags": [], "steps": [ { "key": "mobileOtp", "type": "MobileOtp", "status": "COMPLETED", "attempts": 1, "completedAt": "2026-10-01T03:40:33.192Z" }, { "key": "pan", "type": "PanVerification", "status": "COMPLETED", "attempts": 1, "completedAt": "2026-10-01T03:40:33.237Z" } ], "documents": [ { "documentId": "doc_01M3TRSCCSKFRSR85W59", "type": "SELFIE", "contentType": "image/jpeg", "sizeBytes": 2504, "scanStatus": "CLEAN", "uploadedAt": "2026-10-01T03:40:33.563Z" } ], "submittedAt": "2026-10-01T03:40:33.604Z", "completedAt": null }(The
stepslist is shortened here.) Identifiers are masked by default. When a reviewer approves or rejects in the portal,statusbecomesAPPROVEDorREJECTED,decisionis filled in and you receiveonboarding.approvedoronboarding.rejected.Result is not ready until submit
Calling
/resultbefore the applicant submits returns409 RESULT_NOT_READY. UseGET /v1/onboarding-requests/{requestId}for progress while the applicant is still working.
What next
- Pick your delivery path in the integration guide: hosted redirect, embedded SDK, mobile WebView or API-only.
- Harden your webhook receiver with signature verification and dedupe on
eventId. - Walk the go-live checklist before switching to
vm_live_keys.