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_... or vm_live_... (API key), or vm_at_... (OAuth2 token). See Authentication
Content type
Requests with a body use application/json (the token endpoint uses application/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 own X-Request-Id (8 to 64 letters, digits, _ or -) to correlate; the same value is returned as traceId in 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) send Cache-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.

ParameterDefaultDescription
limit25Page size, 1 to 100
offset0Number 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.

SituationResult
First use of a keyNormal processing, 201
Same key, same bodyStored response replayed, header Idempotent-Replayed: true
Same key, different body409 IDEMPOTENCY_KEY_REUSED
Same key while the first call is still running409 IDEMPOTENCY_IN_PROGRESS. Retry shortly
Missing or malformed key400 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.

HTTPcodeTypical cause
400VERIFYME_VALIDATION_ERRORA field is missing or invalid; see details
400VERIFYME_INVALID_JSONBody is not valid JSON
401VERIFYME_UNAUTHENTICATEDMissing, malformed, revoked, expired or wrong-environment credential
403VERIFYME_INSUFFICIENT_SCOPEThe credential lacks the scope named in the message
403VERIFYME_FORBIDDENSource IP not in the allow-list, or a document that is not available
403VERIFYME_TENANT_SUSPENDEDThe workspace cannot create requests
404VERIFYME_NOT_FOUNDUnknown id, id of another workspace, or unknown route
409IDEMPOTENCY_KEY_REUSED, IDEMPOTENCY_IN_PROGRESS, DUPLICATE_EXTERNAL_REFERENCE, RESULT_NOT_READY, VERIFYME_INVALID_STATE, VERIFYME_STATE_CONFLICTState or idempotency conflicts
413VERIFYME_PAYLOAD_TOO_LARGEJSON body over 256 KB
422FLOW_NOT_FOUND, ENDPOINT_LIMITSemantically invalid
429VERIFYME_RATE_LIMITEDOver the rate limit. Read Retry-After
500VERIFYME_INTERNAL_ERROROur fault. Retry with backoff; quote traceId
503VERIFYME_DEPENDENCY_UNAVAILABLEA dependency is temporarily unavailable. Retry

Authentication endpoints

Get an access token

POST/v1/oauth/token

Exchange an API key for a one-hour OAuth2 bearer token (client credentials grant). Body is form-encoded.

FieldDescription
grant_type requiredMust be client_credentials
client_id requiredThe key prefix, for example vm_test_EeoZZxa3
client_secret requiredThe 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-UaCEifKpd2X8mQw
const 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

GET/v1/ping

Checks 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

POST/v1/onboarding-requestsScope onboarding.create

Creates a request for one applicant and returns the link to open. Requires the Idempotency-Key header.

FieldTypeDescription
externalReference requiredstring, max 200Your own id for this applicant or order. Echoed in events, results and the return URL
applicantType optionalstring, max 60[A-Za-z0-9_.-]+. Default applicant. Your own category such as merchant
mobile optionalstringIndian mobile, 10 digits starting 6 to 9 (a leading +91 or 0 is stripped). If set, the OTP step uses and locks it
email optionalstringValid address, stored lower-cased. Used by the email OTP step
flowKey optionalstring, max 80Flow to run. Default: your tenant's default flow
returnUrl optionalstring, max 500Where 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 optionalinteger 1 to 720Link lifetime. Default: workspace setting (72 hours)
locale optionalstringOne of en, hi, ta, te, kn, mr, bn, gu
metadata optionalobject, up to 4,000 bytesFree-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();   // 201
r = 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()                   # 201

Response 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"
}
StatusCodeWhen
400VERIFYME_VALIDATION_ERRORInvalid 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)
403VERIFYME_TENANT_SUSPENDEDThe workspace is not active
409DUPLICATE_EXTERNAL_REFERENCEDuplicate policy block_active and an active request exists for this externalReference
409IDEMPOTENCY_KEY_REUSED / IDEMPOTENCY_IN_PROGRESSSee Idempotency
422FLOW_NOT_FOUNDUnknown or unpublished flowKey, or no default flow

List onboarding requests

GET/v1/onboarding-requestsScope onboarding.read
QueryDescription
status optionalOne or more statuses, comma separated: SUBMITTED,REVIEWING
externalReference optionalSearch term. Matches externalReference containing the text, an exact requestId, or an exact mobile number
from, to optionalISO 8601 bounds for createdOn (inclusive)
limit, offset optionalPagination (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

GET/v1/onboarding-requests/{requestId}Scope onboarding.read

Current 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

GET/v1/onboarding-requests/{requestId}/resultScope onboarding.result

The verified data, flags, step outcomes, documents and decision. Available once the applicant has submitted (statuses SUBMITTED, REVIEWING, REVERTED, APPROVED, REJECTED). Every call is audited.

QueryDescription
fields optionalComma-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"
}
FieldNotes
decisionnull 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

GET/v1/onboarding-requests/{requestId}/documents/{documentId}Scope onboarding.result

Returns 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

POST/v1/onboarding-requests/{requestId}/cancelScope onboarding.cancel

Cancels 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.

POST/v1/onboarding-requests/{requestId}/reissue-linkScope onboarding.create

Issues 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

GET/v1/flowsScope onboarding.read

Lists 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

POST/v1/webhook-endpointsScope webhook.manage
FieldTypeDescription
url requiredstring, max 500Public HTTPS URL (HTTP allowed only in the sandbox). No embedded credentials. Hostname must resolve to public addresses
events optionalstring arrayEvent 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 now
ep = 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

GET/v1/webhook-endpointsScope webhook.manage
curl -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

PATCH/v1/webhook-endpoints/{endpointId}Scope webhook.manage

Send 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

DELETE/v1/webhook-endpoints/{endpointId}Scope webhook.manage

Returns 204 No Content. Pending deliveries for the endpoint are dropped.

Send a test event

POST/v1/webhook-endpoints/{endpointId}/testScope webhook.manage

Queues a webhook.test event to this endpoint only. Returns 202 Accepted.

{ "deliveryId": "del_01M3TRSXZRKYRSFDPCG4" }

List deliveries

GET/v1/webhook-eventsScope webhook.manage

Event type catalogue and the latest 50 deliveries.

QueryDescription
status optionalPENDING, SUCCESS or DEAD
endpointId optionalOnly 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.

StatusCodeMeaning
402VERIFYME_INSUFFICIENT_BALANCEPrepaid wallet cannot cover the price plus GST. Nothing was called.
403API_NOT_ENABLEDYour price book has no price for this API. Contact support.
503PROVIDER_UNAVAILABLEThe verification partner failed or timed out. You are not charged. Retry after the Retry-After seconds.

Verify a PAN

POST/v1/verify/panScope verify.use

Checks 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

POST/v1/verify/bank-accountScope verify.use

Penny-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

POST/v1/verify/face-matchScope verify.use

Compares 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

POST/v1/verify/livenessScope verify.use

Send 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

GET/v1/lookup/pincode/{pincode}Scope verify.use

Returns 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

POST/v1/otp/mobile/sendScope verify.use

Sends 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

POST/v1/otp/mobile/verifyScope verify.use

Checking 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

POST/v1/otp/email/sendScope verify.use

Same 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

POST/v1/otp/email/verifyScope verify.use

Identical to the mobile verify call; free.

{ "otpId": "otp_01M4...", "otp": "123456" }
{ "verified": true }

Get your prices

GET/v1/pricingScope billing.read

Your 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

GET/v1/usageScope billing.read

Metered 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.

QueryDescription
period optionalBilling 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

EndpointScope
POST /v1/onboarding-requestsonboarding.create
GET /v1/onboarding-requests, GET /v1/onboarding-requests/{id}, GET /v1/flowsonboarding.read
GET .../{id}/result, GET .../{id}/documents/{documentId}onboarding.result
POST .../{id}/cancelonboarding.cancel
POST .../{id}/reissue-linkonboarding.create
/v1/webhook-endpoints, GET /v1/webhook-eventswebhook.manage
GET /v1/usage, GET /v1/pricingbilling.read
POST /v1/verify/*, GET /v1/lookup/pincode/{pincode}, POST /v1/otp/*verify.use
GET /v1/pingany valid credential
POST /v1/oauth/tokennone (client credentials)
Docs version 1.0.0API version v1Last updated 1 Oct 2026