Reference
API reference
The complete VerifyMe REST API, version v1. Every endpoint with its method, path, required scope, parameters, request body, example request in cURL, Node.js and Python, example response and error codes. A machine-readable OpenAPI document is available at /openapi.json.
OpenAPI
Download the machine-readable spec: /openapi.json (OpenAPI 3). Use it to generate clients or import into Postman and Insomnia. If this page and the spec differ, tell us; the spec is generated from the running service.
Conventions
- Base URL
https://kyc.example.com/v1(your deployment host; sandbox and production are different hosts)- Authentication
Authorization: Bearer vm_test_...orvm_live_...(API key), orvm_at_...(OAuth2 token). See Authentication- Content type
- Requests with a body use
application/json(the token endpoint usesapplication/x-www-form-urlencoded). Maximum JSON body 256 KB - Responses
- JSON, UTF-8. Timestamps are ISO 8601 UTC strings (
2026-10-01T03:40:18.571Z). Money is in paise (INR minor units) - Identifiers
- Prefixed opaque strings:
vm_req_...(request),doc_...(document),whe_...(webhook endpoint),evt_...(event),del_...(delivery),trc_...(trace) - Request id
- Every response carries
X-Request-Id. Send your ownX-Request-Id(8 to 64 letters, digits,_or-) to correlate; the same value is returned astraceIdin errors - Rate limit headers
X-RateLimit-Limit,X-RateLimit-Remaining,X-RateLimit-Reset(seconds). See Errors and limits- Caching
- Sensitive responses (
/result, documents, re-issued links, token) sendCache-Control: no-store
Versioning
The version is in the path (/v1). Within v1 changes are additive: new endpoints, optional request fields, new response fields, new event types and new data fields. Write tolerant readers: ignore unknown fields and unknown event types. A breaking change would ship as /v2 with an overlap period. Webhook payloads carry schemaVersion (currently "1").
Pagination
List endpoints use offset pagination.
| Parameter | Default | Description |
|---|---|---|
limit | 25 | Page size, 1 to 100 |
offset | 0 | Number of items to skip |
Responses contain total (all matches) and items (the page). Order is newest first (createdOn descending). Iterate until offset + items.length >= total. GET /v1/webhook-events returns the latest 50 deliveries and has no paging parameters.
Idempotency
POST /v1/onboarding-requests requires an Idempotency-Key header (8 to 120 characters from A-Za-z0-9_.:-). The first response is stored for 24 hours per workspace and endpoint.
| Situation | Result |
|---|---|
| First use of a key | Normal processing, 201 |
| Same key, same body | Stored response replayed, header Idempotent-Replayed: true |
| Same key, different body | 409 IDEMPOTENCY_KEY_REUSED |
| Same key while the first call is still running | 409 IDEMPOTENCY_IN_PROGRESS. Retry shortly |
| Missing or malformed key | 400 VERIFYME_VALIDATION_ERROR with details[0].field = "Idempotency-Key" |
| Original call failed (4xx/5xx) | Nothing is stored; retrying the same key runs it again |
Other endpoints are naturally idempotent: cancel can be repeated, DELETE returns 204, PATCH sets state.
Errors
Failures use one envelope and an HTTP status that matches the class of error:
{
"error": {
"code": "VERIFYME_VALIDATION_ERROR",
"message": "One or more fields are invalid.",
"details": [ { "field": "email", "code": "INVALID_EMAIL" } ],
"traceId": "trc_01M3TSWNY8SNW6A1C89Y"
}
}
code is stable and meant for programs; message is for humans and may change; details appears for validation problems (field plus a machine code); traceId identifies the call for support. The common codes are below; the full catalogue is in Errors and limits.
| HTTP | code | Typical cause |
|---|---|---|
| 400 | VERIFYME_VALIDATION_ERROR | A field is missing or invalid; see details |
| 400 | VERIFYME_INVALID_JSON | Body is not valid JSON |
| 401 | VERIFYME_UNAUTHENTICATED | Missing, malformed, revoked, expired or wrong-environment credential |
| 403 | VERIFYME_INSUFFICIENT_SCOPE | The credential lacks the scope named in the message |
| 403 | VERIFYME_FORBIDDEN | Source IP not in the allow-list, or a document that is not available |
| 403 | VERIFYME_TENANT_SUSPENDED | The workspace cannot create requests |
| 404 | VERIFYME_NOT_FOUND | Unknown id, id of another workspace, or unknown route |
| 409 | IDEMPOTENCY_KEY_REUSED, IDEMPOTENCY_IN_PROGRESS, DUPLICATE_EXTERNAL_REFERENCE, RESULT_NOT_READY, VERIFYME_INVALID_STATE, VERIFYME_STATE_CONFLICT | State or idempotency conflicts |
| 413 | VERIFYME_PAYLOAD_TOO_LARGE | JSON body over 256 KB |
| 422 | FLOW_NOT_FOUND, ENDPOINT_LIMIT | Semantically invalid |
| 429 | VERIFYME_RATE_LIMITED | Over the rate limit. Read Retry-After |
| 500 | VERIFYME_INTERNAL_ERROR | Our fault. Retry with backoff; quote traceId |
| 503 | VERIFYME_DEPENDENCY_UNAVAILABLE | A dependency is temporarily unavailable. Retry |
Authentication endpoints
Get an access token
/v1/oauth/tokenExchange an API key for a one-hour OAuth2 bearer token (client credentials grant). Body is form-encoded.
| Field | Description |
|---|---|
grant_type required | Must be client_credentials |
client_id required | The key prefix, for example vm_test_EeoZZxa3 |
client_secret required | The secret part of the key after the prefix |
curl -X POST https://kyc.example.com/v1/oauth/token \
-d grant_type=client_credentials \
-d client_id=vm_test_EeoZZxa3 \
--data-urlencode client_secret=R0j4wviRzYMghPw3FiZ4_Fzd4DF-UaCEifKpd2X8mQwconst res = await fetch("https://kyc.example.com/v1/oauth/token", {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({ grant_type: "client_credentials", client_id: "vm_test_EeoZZxa3", client_secret: secret }),
});
const { access_token, expires_in } = await res.json();r = requests.post("https://kyc.example.com/v1/oauth/token",
data={"grant_type": "client_credentials", "client_id": "vm_test_EeoZZxa3", "client_secret": secret}, timeout=15)
token = r.json()["access_token"]{
"access_token": "vm_at_QtNRWxcZbpd1tBNLdfG9-GfcIqE6ZW8eHWKeXlxwqgM",
"token_type": "Bearer",
"expires_in": 3600,
"scope": "onboarding.create onboarding.read onboarding.result onboarding.cancel webhook.manage billing.read"
}
Errors: 400 VERIFYME_VALIDATION_ERROR (UNSUPPORTED_GRANT), 401 VERIFYME_UNAUTHENTICATED (bad credentials).
Ping
/v1/pingChecks credentials and returns the environment and the scopes of the calling credential.
curl -s https://kyc.example.com/v1/ping -H "Authorization: Bearer $VERIFYME_API_KEY"
{ "ok": true, "environment": "test", "tenantId": "tnt_01M3TRRAAAS88X5T0X1F",
"scopes": ["onboarding.create", "onboarding.read", "onboarding.result", "onboarding.cancel"] }
Onboarding requests
Create an onboarding request
/v1/onboarding-requestsScope onboarding.createCreates a request for one applicant and returns the link to open. Requires the Idempotency-Key header.
| Field | Type | Description |
|---|---|---|
externalReference required | string, max 200 | Your own id for this applicant or order. Echoed in events, results and the return URL |
applicantType optional | string, max 60 | [A-Za-z0-9_.-]+. Default applicant. Your own category such as merchant |
mobile optional | string | Indian mobile, 10 digits starting 6 to 9 (a leading +91 or 0 is stripped). If set, the OTP step uses and locks it |
email optional | string | Valid address, stored lower-cased. Used by the email OTP step |
flowKey optional | string, max 80 | Flow to run. Default: your tenant's default flow |
returnUrl optional | string, max 500 | Where the browser goes after submit. HTTPS in production, no credentials, not javascript:, data:, vbscript: or file:, host on your return URL allow-list. Custom app schemes allowed |
expiresInHours optional | integer 1 to 720 | Link lifetime. Default: workspace setting (72 hours) |
locale optional | string | One of en, hi, ta, te, kn, mr, bn, gu |
metadata optional | object, up to 4,000 bytes | Free-form JSON stored with the request and visible to your team in the portal. Not returned by the API in 1.0.0 |
curl -X POST https://kyc.example.com/v1/onboarding-requests \
-H "Authorization: Bearer $VERIFYME_API_KEY" \
-H "Idempotency-Key: ord-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("https://kyc.example.com/v1/onboarding-requests", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.VERIFYME_API_KEY}`,
"Idempotency-Key": "ord-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" },
}),
});
const request = await res.json(); // 201r = requests.post(
"https://kyc.example.com/v1/onboarding-requests",
headers={"Authorization": f"Bearer {KEY}", "Idempotency-Key": "ord-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,
)
request = r.json() # 201Response 201 Created:
{
"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"
}
| Status | Code | When |
|---|---|---|
| 400 | VERIFYME_VALIDATION_ERROR | Invalid field. details[].code: REQUIRED, INVALID_EMAIL, INVALID_MOBILE, INVALID_URL, UNSAFE_SCHEME, HTTPS_REQUIRED, HOST_NOT_ALLOWED, INVALID_NUMBER, INVALID_VALUE, REQUIRED_OR_INVALID (idempotency key) |
| 403 | VERIFYME_TENANT_SUSPENDED | The workspace is not active |
| 409 | DUPLICATE_EXTERNAL_REFERENCE | Duplicate policy block_active and an active request exists for this externalReference |
| 409 | IDEMPOTENCY_KEY_REUSED / IDEMPOTENCY_IN_PROGRESS | See Idempotency |
| 422 | FLOW_NOT_FOUND | Unknown or unpublished flowKey, or no default flow |
List onboarding requests
/v1/onboarding-requestsScope onboarding.read| Query | Description |
|---|---|
status optional | One or more statuses, comma separated: SUBMITTED,REVIEWING |
externalReference optional | Search term. Matches externalReference containing the text, an exact requestId, or an exact mobile number |
from, to optional | ISO 8601 bounds for createdOn (inclusive) |
limit, offset optional | Pagination (see above) |
curl -s "https://kyc.example.com/v1/onboarding-requests?status=SUBMITTED&limit=25" \
-H "Authorization: Bearer $VERIFYME_API_KEY"const url = new URL("https://kyc.example.com/v1/onboarding-requests");
url.search = new URLSearchParams({ status: "SUBMITTED", limit: "25" });
const page = await (await fetch(url, { headers: { Authorization: `Bearer ${KEY}` } })).json();page = requests.get(
"https://kyc.example.com/v1/onboarding-requests",
params={"status": "SUBMITTED", "limit": 25},
headers={"Authorization": f"Bearer {KEY}"}, timeout=15,
).json(){
"total": 1,
"items": [
{
"requestId": "vm_req_01M3TRSBZTTWMHGGTTGP",
"externalReference": "EMP-77",
"applicantType": "applicant",
"status": "SUBMITTED",
"reviewStatus": "PENDING",
"mobileMasked": "98XXXXXX34",
"flagCount": 0,
"createdOn": "2026-10-01T03:40:33.146Z",
"submittedOn": "2026-10-01T03:40:33.604Z",
"expiresOn": "2026-10-04T03:40:33.146Z"
}
]
}
List items use createdOn, submittedOn and expiresOn, whereas the single-request view below uses createdAt, submittedAt and expiresAt. reviewStatus is one of NONE, PENDING, IN_REVIEW, AWAITING_CHECKER, APPROVED, REJECTED, REVERTED.
Get an onboarding request
/v1/onboarding-requests/{requestId}Scope onboarding.readCurrent status and progress. Safe to poll.
curl -s https://kyc.example.com/v1/onboarding-requests/vm_req_01M3TRSBZTTWMHGGTTGP \
-H "Authorization: Bearer $VERIFYME_API_KEY"const status = await (await fetch(`${HOST}/v1/onboarding-requests/${requestId}`, {
headers: { Authorization: `Bearer ${KEY}` },
})).json();status = requests.get(f"{HOST}/v1/onboarding-requests/{request_id}",
headers={"Authorization": f"Bearer {KEY}"}, timeout=15).json(){
"requestId": "vm_req_01M3TRSBZTTWMHGGTTGP",
"externalReference": "EMP-77",
"status": "SUBMITTED",
"progress": { "completed": 5, "total": 5, "stepsCompleted": 5, "stepsTotal": 5 },
"review": { "status": "PENDING" },
"createdAt": "2026-10-01T03:40:33.146Z",
"updatedAt": "2026-10-01T03:40:33.604Z",
"expiresAt": "2026-10-04T03:40:33.146Z",
"submittedAt": "2026-10-01T03:40:33.604Z",
"completedAt": null
}
progress.completed and progress.total count required steps; stepsCompleted and stepsTotal count all steps. completedAt is set when the request reaches a final status. A 404 VERIFYME_NOT_FOUND is returned for unknown ids and for ids of other workspaces.
Get the result
/v1/onboarding-requests/{requestId}/resultScope onboarding.resultThe verified data, flags, step outcomes, documents and decision. Available once the applicant has submitted (statuses SUBMITTED, REVIEWING, REVERTED, APPROVED, REJECTED). Every call is audited.
| Query | Description |
|---|---|
fields optional | Comma-separated keys of verified to return, for example fullName,pan. Omit for all |
curl -s "https://kyc.example.com/v1/onboarding-requests/vm_req_01M3TRSBZTTWMHGGTTGP/result?fields=fullName,pan,faceMatchPassed" \
-H "Authorization: Bearer $VERIFYME_API_KEY"const r = await fetch(`${HOST}/v1/onboarding-requests/${requestId}/result?fields=fullName,pan`, {
headers: { Authorization: `Bearer ${KEY}` },
});
if (r.status === 409) return null; // RESULT_NOT_READY
const result = await r.json();r = requests.get(f"{HOST}/v1/onboarding-requests/{request_id}/result",
params={"fields": "fullName,pan"}, headers={"Authorization": f"Bearer {KEY}"}, timeout=15)
if r.status_code == 409:
result = None # RESULT_NOT_READY
else:
result = r.json(){
"requestId": "vm_req_01M3TRSBZTTWMHGGTTGP",
"externalReference": "EMP-77",
"status": "APPROVED",
"decision": { "outcome": "APPROVED", "reasonCode": "VERIFIED_OK", "decidedAt": "2026-10-01T05:12:07.442Z" },
"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": [ { "code": "PAN_NAME_MISMATCH", "step": "pan", "severity": "MEDIUM" } ],
"steps": [
{ "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": "2026-10-01T05:12:07.442Z"
}
| Field | Notes |
|---|---|
decision | null until a reviewer or policy approves or rejects. outcome is APPROVED or REJECTED; reasonCode is one of the review reason codes |
verified.* | Keys not collected by the flow are null. mobile, email, pan, aadhaar and bankAccount are masked. Name, date of birth and address are returned in full |
flags[] | Active risk flags with code, step and severity (HIGH, MEDIUM, LOW, INFO). See Flows and steps |
steps[] | One entry per step with status (PENDING, IN_PROGRESS, PROVIDER_PENDING, FAILED_RETRYABLE, COMPLETED, FAILED, SKIPPED, REVERTED) |
documents[] | Metadata only. Fetch content with the documents endpoint |
Errors: 409 RESULT_NOT_READY, 404 VERIFYME_NOT_FOUND, 403 VERIFYME_INSUFFICIENT_SCOPE.
Get a document
/v1/onboarding-requests/{requestId}/documents/{documentId}Scope onboarding.resultReturns a short-lived signed URL for a stored document or photo. Only files that passed the malware scan are available. Each call is audited.
curl -s https://kyc.example.com/v1/onboarding-requests/vm_req_01M3TRSBZTTWMHGGTTGP/documents/doc_01M3TRSCCSKFRSR85W59 \
-H "Authorization: Bearer $VERIFYME_API_KEY"
{
"documentId": "doc_01M3TRSCCSKFRSR85W59",
"type": "SELFIE",
"contentType": "image/jpeg",
"url": "https://kyc.example.com/files/dGVuYW50cy9...?exp=1790826171&ct=image%2Fjpeg&dl=0&sig=907751e4...",
"expiresInSec": 120
}
The URL is valid for 120 seconds and needs no further authentication, so fetch it immediately from your server and do not log or store it. After expiry the file endpoint returns 403 VERIFYME_URL_EXPIRED. Errors: 404 VERIFYME_NOT_FOUND (unknown or deleted document), 403 VERIFYME_FORBIDDEN (not yet scanned clean).
Cancel a request
/v1/onboarding-requests/{requestId}/cancelScope onboarding.cancelCancels a request that is not final. Open applicant sessions are revoked and onboarding.cancelled is emitted with data.by = "API_CLIENT". Idempotent: cancelling an already cancelled request returns it unchanged. No body.
curl -s -X POST https://kyc.example.com/v1/onboarding-requests/vm_req_01M3TRSY19NBF82HA679/cancel \
-H "Authorization: Bearer $VERIFYME_API_KEY"
{
"requestId": "vm_req_01M3TRSY19NBF82HA679",
"externalReference": "NR-1",
"status": "CANCELLED",
"progress": { "completed": 0, "total": 6, "stepsCompleted": 0, "stepsTotal": 11 },
"review": { "status": "NONE" },
"createdAt": "2026-10-01T03:40:51.625Z",
"updatedAt": "2026-10-01T03:40:51.688Z",
"expiresAt": "2026-10-04T03:40:51.625Z",
"submittedAt": null,
"completedAt": "2026-10-01T03:40:51.688Z"
}
Errors: 409 VERIFYME_INVALID_STATE if the request is already APPROVED, REJECTED or EXPIRED.
Re-issue the link
/v1/onboarding-requests/{requestId}/reissue-linkScope onboarding.createIssues a new onboardingUrl, invalidating the old token and revoking open applicant sessions. Completed evidence is kept. Use after a suspected link leak or when the link went to the wrong address. No body, no idempotency key.
curl -s -X POST https://kyc.example.com/v1/onboarding-requests/vm_req_01M3TRSY19NBF82HA679/reissue-link \
-H "Authorization: Bearer $VERIFYME_API_KEY"
{
"requestId": "vm_req_01M3TRSY19NBF82HA679",
"onboardingUrl": "https://kyc.example.com/start/vm_t_Bn97uG3qGCuVSKVA2cv8hrFM8UlkMcBzpqYPFra1KkY",
"expiresAt": "2026-10-04T03:40:51.625Z"
}
The expiry does not change. Errors: 409 VERIFYME_INVALID_STATE when the request is SUBMITTED, REVIEWING or final. A MEDIUM security event is recorded.
Flows
List flows
/v1/flowsScope onboarding.readLists your published flows and their steps, so you can choose a flowKey.
curl -s https://kyc.example.com/v1/flows -H "Authorization: Bearer $VERIFYME_API_KEY"
{
"flows": [
{
"flowKey": "bank-account-check-v1",
"name": "Bank Account Check",
"version": 1,
"steps": [
{ "key": "mobileOtp", "type": "MobileOtp", "required": true },
{ "key": "bank", "type": "BankVerification", "required": true },
{ "key": "consent", "type": "Consent", "required": true }
]
}
]
}
Webhook endpoints
Webhook endpoint management requires the webhook.manage scope. See Webhooks for events and signatures. Signing secret rotation is done in the portal.
Create an endpoint
/v1/webhook-endpointsScope webhook.manage| Field | Type | Description |
|---|---|---|
url required | string, max 500 | Public HTTPS URL (HTTP allowed only in the sandbox). No embedded credentials. Hostname must resolve to public addresses |
events optional | string array | Event types to receive, or ["*"]. Default ["*"] |
curl -X POST https://kyc.example.com/v1/webhook-endpoints \
-H "Authorization: Bearer $VERIFYME_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "url": "https://shop.example.com/hooks/verifyme", "events": ["onboarding.approved", "onboarding.rejected"] }'const ep = await (await fetch("https://kyc.example.com/v1/webhook-endpoints", {
method: "POST",
headers: { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" },
body: JSON.stringify({ url: "https://shop.example.com/hooks/verifyme", events: ["onboarding.approved", "onboarding.rejected"] }),
})).json();
// ep.secret is shown only nowep = requests.post("https://kyc.example.com/v1/webhook-endpoints",
headers={"Authorization": f"Bearer {KEY}"},
json={"url": "https://shop.example.com/hooks/verifyme", "events": ["onboarding.approved", "onboarding.rejected"]},
timeout=15).json()
# ep["secret"] is shown only now{
"endpointId": "whe_01M3TRSXX1MWDZ00PMG2",
"url": "https://shop.example.com/hooks/verifyme",
"events": ["onboarding.approved", "onboarding.rejected"],
"status": "ACTIVE",
"consecutiveFailures": 0,
"secretRotating": false,
"createdOn": "2026-10-01T03:40:51.489Z",
"updatedOn": "2026-10-01T03:40:51.489Z",
"secret": "whsec_Yw80IXbqWnq6bPhYcvY27tJ4ooPKXKECVBqeRjeOHOg",
"note": "Store the signing secret securely. It will not be shown again."
}
Response status 201. Errors: 400 VERIFYME_VALIDATION_ERROR with details[].code INVALID_URL, HTTPS_REQUIRED, PRIVATE_ADDRESS_NOT_ALLOWED, UNKNOWN_EVENT; 422 ENDPOINT_LIMIT (maximum 10 endpoints).
List endpoints
/v1/webhook-endpointsScope webhook.managecurl -s https://kyc.example.com/v1/webhook-endpoints -H "Authorization: Bearer $VERIFYME_API_KEY"
{ "endpoints": [ { "endpointId": "whe_01M3TRSXX1MWDZ00PMG2", "url": "https://shop.example.com/hooks/verifyme",
"events": ["onboarding.approved", "onboarding.rejected"], "status": "ACTIVE", "consecutiveFailures": 0,
"secretRotating": false, "createdOn": "2026-10-01T03:40:51.489Z", "updatedOn": "2026-10-01T03:40:51.489Z" } ] }
The secret is never returned again. status is ACTIVE or DISABLED; secretRotating is true while the previous secret is still accepted.
Update an endpoint
/v1/webhook-endpoints/{endpointId}Scope webhook.manageSend any of url, events or status (ACTIVE or DISABLED). Setting ACTIVE resets consecutiveFailures.
curl -X PATCH https://kyc.example.com/v1/webhook-endpoints/whe_01M3TRSXX1MWDZ00PMG2 \
-H "Authorization: Bearer $VERIFYME_API_KEY" -H "Content-Type: application/json" \
-d '{ "status": "ACTIVE" }'
Returns the endpoint object (as in the list). Errors: 404, 400 (INVALID, UNKNOWN_EVENT, URL errors).
Delete an endpoint
/v1/webhook-endpoints/{endpointId}Scope webhook.manageReturns 204 No Content. Pending deliveries for the endpoint are dropped.
Send a test event
/v1/webhook-endpoints/{endpointId}/testScope webhook.manageQueues a webhook.test event to this endpoint only. Returns 202 Accepted.
{ "deliveryId": "del_01M3TRSXZRKYRSFDPCG4" }
List deliveries
/v1/webhook-eventsScope webhook.manageEvent type catalogue and the latest 50 deliveries.
| Query | Description |
|---|---|
status optional | PENDING, SUCCESS or DEAD |
endpointId optional | Only deliveries to this endpoint |
{
"eventTypes": ["onboarding.created", "onboarding.started", "onboarding.step.completed", "onboarding.submitted",
"onboarding.reverted", "onboarding.approved", "onboarding.rejected", "onboarding.expired",
"onboarding.cancelled", "verification.started", "verification.completed", "verification.failed",
"verification.pending", "document.uploaded", "document.rejected", "document.expired", "webhook.test"],
"deliveries": [
{
"deliveryId": "del_01M3TRSXZRKYRSFDPCG4",
"eventId": "evt_01M3TRSXZ0S6W2P3H9QK",
"eventType": "webhook.test",
"requestId": null,
"endpointId": "whe_01M3TRSXX1MWDZ00PMG2",
"attempt": 1,
"status": "SUCCESS",
"statusCode": 200,
"durationMs": 84,
"nextRetryOn": null,
"deliveredOn": "2026-10-01T03:40:52.101Z",
"lastError": null,
"createdOn": "2026-10-01T03:40:51.532Z"
}
]
}
Verification APIs
Use the same verification services that power journeys as standalone calls. Each call is charged at your price book's API price plus GST and the response says exactly what was charged in charge. Nothing you send is stored. Send an optional Idempotency-Key header (8 to 120 characters) to make a retried call free. Needs scope verify.use, which is not part of the default key scopes.
| Status | Code | Meaning |
|---|---|---|
| 402 | VERIFYME_INSUFFICIENT_BALANCE | Prepaid wallet cannot cover the price plus GST. Nothing was called. |
| 403 | API_NOT_ENABLED | Your price book has no price for this API. Contact support. |
| 503 | PROVIDER_UNAVAILABLE | The verification partner failed or timed out. You are not charged. Retry after the Retry-After seconds. |
Verify a PAN
/v1/verify/panScope verify.useChecks a PAN with the registry. Optionally compares the holder name with a name you provide (order, initials and a missing middle name are tolerated; default threshold 70).
{ "pan": "ABCPE1234F", "name": "Ramesh Kumar Sharma" }
{
"valid": true,
"panMasked": "ABXXXXXX4F",
"holderName": "RAMESH KUMAR SHARMA",
"category": "Individual",
"aadhaarLinked": true,
"nameMatch": { "score": 100, "matched": true, "threshold": 70 },
"charge": { "item": "API.PAN_VERIFICATION", "charged": true, "netPaise": 200, "gstPaise": 36, "totalPaise": 236, "gstRatePercent": 18, "currency": "INR" }
}
Verify a bank account
/v1/verify/bank-accountScope verify.usePenny-drop style verification of an account number and IFSC, with an optional holder-name comparison.
{ "accountNumber": "123456789012", "ifsc": "HDFC0001234", "name": "Ramesh Sharma" }
{
"valid": true,
"accountMasked": "XXXXXXXX9012",
"ifsc": "HDFC0001234",
"holderName": "RAMESH SHARMA",
"bankName": "HDFC Bank",
"nameMatch": { "score": 100, "matched": true, "threshold": 70 },
"charge": { "item": "API.BANK_VERIFICATION", "charged": true, "netPaise": 200, "gstPaise": 36, "totalPaise": 236, "gstRatePercent": 18, "currency": "INR" }
}
Match a selfie to a photo
/v1/verify/face-matchScope verify.useCompares two images, sent as base64 (JPEG or PNG, up to 2 MB each). threshold is optional (1 to 99, default 60).
{ "selfieBase64": "<base64>", "referenceBase64": "<base64>", "threshold": 60 }
{
"matched": true,
"score": 87.5,
"threshold": 60,
"confidence": 0.93,
"charge": { "item": "API.FACE_MATCH", "charged": true, "netPaise": 200, "gstPaise": 36, "totalPaise": 236, "gstRatePercent": 18, "currency": "INR" }
}
Check liveness
/v1/verify/livenessScope verify.useSend 3 to 6 frames (base64 JPEG or PNG, up to 2 MB each) captured in sequence.
{ "frames": ["<base64>", "<base64>", "<base64>"] }
{
"live": true,
"score": 0.97,
"charge": { "item": "API.VIDEO_LIVENESS", "charged": true, "netPaise": 200, "gstPaise": 36, "totalPaise": 236, "gstRatePercent": 18, "currency": "INR" }
}
Look up a pincode
/v1/lookup/pincode/{pincode}Scope verify.useReturns the city and state for a six-digit Indian pincode.
{
"found": true,
"pincode": "500081",
"city": "Hyderabad",
"state": "Telangana",
"charge": { "item": "API.PINCODE_LOOKUP", "charged": true, "netPaise": 200, "gstPaise": 36, "totalPaise": 236, "gstRatePercent": 18, "currency": "INR" }
}
Send a mobile OTP
/v1/otp/mobile/sendScope verify.useSends a six-digit code by SMS. The code is valid for 5 minutes and allows 3 tries. Sandbox keys also get sandboxCode in the response; numbers ending 1234 always get 123456.
{ "mobile": "9876543210" }
{
"otpId": "otp_01M4...",
"sent": true,
"target": "XXXXXX3210",
"expiresInSec": 300,
"charge": { "item": "API.MOBILE_OTP", "charged": true, "netPaise": 200, "gstPaise": 36, "totalPaise": 236, "gstRatePercent": 18, "currency": "INR" }
}
Verify a mobile OTP
/v1/otp/mobile/verifyScope verify.useChecking a code is free. A code works once.
{ "otpId": "otp_01M4...", "otp": "123456" }
{ "verified": true }
// or
{ "verified": false, "reason": "INCORRECT_CODE", "attemptsLeft": 2 } // also EXPIRED_OR_UNKNOWN, ATTEMPTS_EXCEEDED
Send an e-mail OTP
/v1/otp/email/sendScope verify.useSame as the mobile OTP, delivered by e-mail. Body: { "email": "a@b.com" }.
{ "email": "ramesh@example.com" }
{ "otpId": "otp_01M4...", "sent": true, "target": "r****@example.com", "expiresInSec": 300, "charge": { "item": "API.EMAIL_OTP", "charged": true, "netPaise": 200, "gstPaise": 36, "totalPaise": 236, "gstRatePercent": 18, "currency": "INR" } }
Verify an e-mail OTP
/v1/otp/email/verifyScope verify.useIdentical to the mobile verify call; free.
{ "otpId": "otp_01M4...", "otp": "123456" }
{ "verified": true }
Get your prices
/v1/pricingScope billing.readYour price book: what each journey and each API costs, with GST. Amounts are in paise. An API with priced: false is not enabled for your account.
{
"priceBook": { "code": "growth", "name": "Growth" },
"gstRatePercent": 18,
"sacCode": "998314",
"placeOfSupply": "36",
"intraState": true,
"includedAttemptsPerStep": 1,
"flows": [ { "flowKey": "bank-account-check-v1", "name": "Bank Account Check", "priced": true, "netPaise": 1900, "gstPaise": 342, "totalPaise": 2242 } ],
"apis": [ { "item": "API.PAN_VERIFICATION", "label": "PAN verification", "priced": true, "netPaise": 200, "gstPaise": 36, "totalPaise": 236 } ]
}
Usage
Get usage and estimated charges
/v1/usageScope billing.readMetered usage for a billing period with the plan, included credits and an estimated subtotal. Amounts are in paise (₹1 = 100 paise). Every row shows net (our price), GST and total.
| Query | Description |
|---|---|
period optional | Billing period YYYYMM, for example 202610. Default: the current UTC month |
{
"billingPeriod": "202610",
"plan": { "code": "sandbox", "name": "Sandbox", "monthlyFeePaise": 0, "currency": "INR", "includedCreditsPaise": 0 },
"byMeter": [
{ "item": "FLOW.bank-account-check-v1", "quantity": 2, "netPaise": 3800, "gstPaise": 684, "totalPaise": 4484 },
{ "item": "API.PAN_VERIFICATION", "quantity": 1, "netPaise": 200, "gstPaise": 36, "totalPaise": 236 }
],
"usagePaise": 4000,
"usageGstPaise": 720,
"usageTotalPaise": 4720,
"includedCreditsAppliedPaise": 0,
"overagePaise": 0,
"subscriptionPaise": 0,
"estimatedSubtotalPaise": 0,
"currency": "INR",
"daily": [ { "day": "2026-10-01", "quantity": 9, "amountPaise": 0 } ]
}
(plan is shortened; it also contains planId and features.) Items are FLOW.<flowKey> (a journey start) and API.<CODE> (an API call or an extra attempt inside a journey): AADHAAR_VERIFICATION, PAN_VERIFICATION, BANK_VERIFICATION, FACE_MATCH, VIDEO_LIVENESS, DOCUMENT_PROCESSING, PINCODE_LOOKUP, MOBILE_OTP, EMAIL_OTP. Usage is an immutable ledger with price, GST and total captured at the time of the event; later price changes do not rewrite history.
Scope summary
| Endpoint | Scope |
|---|---|
POST /v1/onboarding-requests | onboarding.create |
GET /v1/onboarding-requests, GET /v1/onboarding-requests/{id}, GET /v1/flows | onboarding.read |
GET .../{id}/result, GET .../{id}/documents/{documentId} | onboarding.result |
POST .../{id}/cancel | onboarding.cancel |
POST .../{id}/reissue-link | onboarding.create |
/v1/webhook-endpoints, GET /v1/webhook-events | webhook.manage |
GET /v1/usage, GET /v1/pricing | billing.read |
POST /v1/verify/*, GET /v1/lookup/pincode/{pincode}, POST /v1/otp/* | verify.use |
GET /v1/ping | any valid credential |
POST /v1/oauth/token | none (client credentials) |