Get started
Sandbox and testing
The sandbox is a full copy of the VerifyMe API and hosted UI with simulated providers. Nothing is billed, no real SMS is sent, and every outcome (success, mismatch, outage) can be triggered with specific test values.
Environments
| Sandbox | Production | |
|---|---|---|
| API keys | vm_test_... | vm_live_... |
| Publishable key | vm_pk_test_... | vm_pk_live_... |
GET /v1/ping environment | test | live |
| Providers (SMS, email, DigiLocker, PAN, bank, face, liveness) | Simulated, deterministic | Real providers bound to your tenant |
| OTP delivery | Code is returned in the response and shown in the UI and developer inbox | Delivered by SMS or email only |
| Webhook targets | localhost and private addresses allowed | Public HTTPS only |
returnUrl | HTTP allowed | HTTPS required |
Events environment field | test | live |
| Billing | Free (sandbox plan) | Metered |
| Data | Test data only. Never upload real identity documents | Real personal data |
Each environment is a separate deployment with its own data. A request, key or webhook endpoint created in the sandbox does not exist in production. The sandbox host is whatever your deployment uses; the samples here use https://kyc.example.com.
Test data
Enter these values in the hosted UI. All of them are deterministic.
Mobile and email OTP
| Input | Behaviour |
|---|---|
| Any valid Indian mobile (10 digits, first digit 6 to 9) | A random six-digit OTP is generated. It is shown on screen as the sandbox code and in the sandbox inbox |
Mobile ending 1234 | The OTP is always 123456 |
Mobile ending 0000 | The simulated SMS gateway times out. The step goes to a delayed state, delivery is retried in the background and the applicant can request a new code |
Email starting fail@ | The simulated mail server is unavailable |
| Wrong code | INVALID_OTP with attemptsLeft. After the attempt limit (3): OTP_ATTEMPTS_EXCEEDED, request a new code |
| Wait more than the expiry (300 seconds SMS, 600 seconds email) | OTP_EXPIRED |
| Ask for another code within 30 seconds | 429 with Retry-After. At most 5 sends per hour per step |
Aadhaar via DigiLocker
The DigiLocker step opens a simulator consent page (/mock/digilocker/{txn}) clearly labelled as a sandbox simulator.
| Action | Result |
|---|---|
| Allow access | Step completes. Name TEST APPLICANT (or the name entered earlier in Personal details), date of birth 1990-05-14 (or the entered value), gender M, address 12, MG Road, Bengaluru, Karnataka 560001, a random Aadhaar last four, masked as XXXX XXXX 1234-style in results |
| Deny | DIGILOCKER_NOT_COMPLETED, retryable |
| Close the window and never answer | The step stays pending (verification.pending) and expires after about 15 minutes; reopen DigiLocker to retry |
PAN
PAN format is five letters, four digits, one letter (ABCPK1234F). The 4th character gives the category (P individual, C company, F firm, H HUF, T trust, A AOP).
| PAN | Result |
|---|---|
| Any valid PAN | Verified, holder name equals the name entered. Category from the 4th character |
5th character T (for example ABCPT1234F) | Registry outage. Step shows PROVIDER_UNAVAILABLE, the attempt is not consumed, flag PROVIDER_UNAVAILABLE (severity INFO) is recorded. It is never turned into a rejection |
Last character X (for example ABCPK1234X) | PAN_INVALID |
5th character Z (for example ABCPZ1234F) | Verified but the holder name is DIFFERENT PERSON: flag PAN_AADHAAR_MISMATCH (HIGH) when DigiLocker ran first, otherwise PAN_NAME_MISMATCH (MEDIUM) |
Bank account
| Account number | Result |
|---|---|
9 to 18 digits, IFSC HDFC0001234 | Verified; bank name HDFC Bank. SBIN... gives State Bank of India, ICIC... ICICI Bank, any other prefix Bank |
Ending 9999 | Verified but the holder is SOME OTHER HOLDER: flag BANK_NAME_MISMATCH (MEDIUM). If the flow has blockOnNameMismatch the step fails with BANK_NAME_MISMATCH |
Starting 0000 | BANK_ACCOUNT_INVALID |
Ending 1111 | Gateway timeout: PROVIDER_UNAVAILABLE, not a rejection |
IFSC format: four letters, a zero, six letters or digits.
Selfie, liveness and uploads
| Input | Result |
|---|---|
| Selfie image (JPEG, PNG or WebP) of 1,500 bytes or more | Score between 72 and 98, passes the default threshold of 60 |
| Selfie image under 1,500 bytes | Score 38. FACE_MISMATCH and retry. After the attempt limit (3 by default) the step completes with flag FACE_BELOW_THRESHOLD (HIGH) and goes to manual review. If the flow sets onFail: block the step fails with FACE_VERIFICATION_FAILED |
| Liveness: 3 to 10 frames totalling more than 3,000 bytes | Live, score 92 |
| Liveness: smaller | LIVENESS_FAILED (HIGH) |
| Document or photo that is not JPEG, PNG, WebP (or PDF where allowed) | FILE_TYPE_NOT_ALLOWED. The type is detected from file content, not the file name |
| File larger than 8 MB | FILE_TOO_LARGE |
| A file containing the standard EICAR test string | Rejected by the malware scan: DOCUMENT_SCAN_FAILED (HIGH) and a security event |
| PDF with embedded JavaScript | DOCUMENT_SCAN_FAILED |
| Geo photo without location when the step requires it | GEO_REQUIRED; otherwise it completes with flag GEO_UNAVAILABLE (LOW) |
Address and pincode
The pincode lookup is simulated from the first two digits: 11 New Delhi, 12 Gurugram, 14 Ludhiana, 17 Shimla, 20 Lucknow, 30 Jaipur, 38 Ahmedabad, 40 Mumbai, 41 Pune, 45 Indore, 49 Raipur, 50 Hyderabad, 56 Bengaluru, 60 Chennai, 64 Coimbatore, 68 Kochi, 70 Kolkata, 75 Bhubaneswar, 78 Guwahati, 80 Patna. Other prefixes return not found, so the applicant fills city and state manually. Pincodes cannot start with 0.
Personal details and consent
- Full name accepts letters, spaces, dots, apostrophes and hyphens. Date of birth is
YYYY-MM-DD; under the flow'sminAge(18) givesUNDERAGE. - The consent step requires
accepted: true; otherwiseCONSENT_REQUIRED.
Sandbox inbox
Because no real SMS or email leaves the sandbox, every message the simulated providers "send" is kept for one hour (the latest 30 per channel) and shown in the portal's developer sandbox inbox. The recipient is masked as it would be in logs. The API behind it is GET /api/portal/sandbox/messages, available only in the sandbox (404 in production). The OTP is also in the step response as sandboxCode, which is what the hosted UI displays under the input.
Webhook sink and simulated events
- Sink:
POST /api/portal/webhooks/sandbox-sink(the portal button) creates an endpoint whose URL is/demo/sink/{endpointId}on the sandbox host. It verifies the signature like a correct consumer and records the last 30 deliveries. See Webhooks. - Simulated events:
POST /api/portal/sandbox/emitwith{ "type": "onboarding.approved", "requestId": "vm_req_..." }emits a single event withdata.simulated: true. Useful to exercise your handler for events that normally need a human (approve, reject, revert). - Real end-to-end: submit a request in the hosted UI, then approve or reject it as a reviewer in the portal's review queue. The review policy of your tenant decides whether decisions are manual (
MANUAL, the default), automatic when no HIGH flag exists (AUTO) or when no flag other than INFO exists (HYBRID).
Simulating failures
| To test | Do this |
|---|---|
| Provider outage (never a false rejection) | PAN with 5th character T, bank account ending 1111, mobile ending 0000 |
| Wrong data / mismatch flags | PAN 5th character Z, bank account ending 9999 |
| Low face match and manual review | Selfie under 1,500 bytes |
| Rejected upload | Upload a text file renamed .jpg, or the EICAR string |
| Link expiry | Create with expiresInHours: 1 and wait; or cancel and re-open the link to see the cancelled page |
| Re-issued link | POST .../reissue-link, then open the old link: it shows "not valid" |
| Rate limiting | Send many requests quickly (default 600 per minute) and read X-RateLimit-* and Retry-After |
| Webhook retries | Point an endpoint at a URL that returns 500, watch attempts at 30 s, 2 min, 10 min in GET /v1/webhook-events |
| Idempotency | Repeat a create with the same Idempotency-Key (replayed) and with a changed body (409 IDEMPOTENCY_KEY_REUSED) |
| Custom domain | In the sandbox, domain verification can be simulated so you can see tenant-domain links without DNS |
Resetting
There is no one-click reset of sandbox data. To start clean: cancel open requests with POST .../cancel, use new externalReference values (or leave the block_active duplicate policy off), revoke old keys, and delete old webhook endpoints. Requests expire by themselves after the TTL.
Going live
- Complete the go-live checklist.
- Replace the sandbox host with the production host and
vm_test_withvm_live_keys, and the publishable key if you embed. - Re-create webhook endpoints in production (they are not copied) and store the new secrets.
- Re-save return URL allow-list and allowed origins for the production workspace.
- Bind real providers and publish your flow in production, then run one real end-to-end verification yourself.
- Remove any sandbox sink endpoints and demo credentials from shared environments.