Concepts
Flows and steps
A flow decides what an applicant has to do. This page catalogues every step type with its configuration, what the applicant sees and what data and flags it produces, then explains flow versions, flags and severities, and review policies.
Flows
A flow is an ordered list of steps plus the legal context of the verification. You choose a flow per request with flowKey, or omit it to use the tenant default. Every new workspace gets three published templates you can use immediately or clone and edit in the portal flow builder:
flowKey | Name | Steps | Review policy |
|---|---|---|---|
india-business-kyc-v1 | India Business KYC | mobileOtp, emailOtp (optional), aadhaar, pan, bank, selfie, shopInside (optional), shopOutside (optional), documents (optional), business (optional), consent | MANUAL |
employee-verification-v1 | Employee Verification | mobileOtp, pan, aadhaar, selfie, consent | HYBRID |
bank-account-check-v1 | Bank Account Check | mobileOtp, bank, consent | AUTO |
List what is live for your workspace:
curl -s "https://kyc.example.com/v1/flows" -H "Authorization: Bearer $VERIFYME_API_KEY"
An unknown or unpublished flowKey returns 422 FLOW_NOT_FOUND.
Flow definition
{
"purposeCode": "KYC_ONBOARDING",
"noticeVersion": "2026-10-01",
"retentionClass": "KYC_RESULT",
"reviewPolicy": "HYBRID",
"locale": "en",
"steps": [
{ "key": "mobileOtp", "type": "MobileOtp", "sequence": 1, "required": true,
"purposeCode": "IDENTITY_VERIFICATION", "config": { "expirySeconds": 300, "maxAttempts": 3 } },
{ "key": "pan", "type": "PanVerification", "sequence": 2, "required": true,
"purposeCode": "IDENTITY_VERIFICATION", "providerBinding": "pan-default" },
{ "key": "consent", "type": "Consent", "sequence": 3, "required": true, "purposeCode": "KYC_ONBOARDING" }
]
}
Validation rules enforced when a flow is published:
purposeCode(uppercase letters and underscores, 3 to 60) andnoticeVersionare required (DPDP purpose limitation and notice).- At most 25 steps. Step
keyis alphanumeric, starts with a letter, 2 to 41 characters and unique.sequenceis an integer. - Every step that collects restricted data needs its own
purposeCode(data minimisation). - A flow with restricted data must include a
Consentstep. - Face match
thresholdmust be an integer from 30 to 99. reviewPolicyisMANUAL,AUTOorHYBRID.
Flow versions
Published versions are immutable. Editing a flow creates a draft (a new version number); publishing it makes it the live version for new requests. A request is pinned to the version that was live when it was created, so changing a flow never alters in-flight applicants. The reviewer and the result API always interpret a request against its own version. Roll forward by publishing again; to stop using a flow, remove it as the default or from your code.
Step catalogue
Steps that need an external provider (SMS, email, DigiLocker, PAN, bank, face, liveness, pincode) use the provider bound to your workspace. The whole journey is charged once, at the journey price, when the applicant starts. Each step below lists the API that covers its provider call; the first attempt per step is included in the journey price, and every further attempt is charged at that API price. The sandbox is free.
| Step type | Provider capability | API (extra attempts) |
|---|---|---|
MobileOtp | sms | none |
EmailOtp | email | none |
PersonalDetails | none | none |
Address | pincode | none |
DigiLockerAadhaar | digilocker | AADHAAR_VERIFICATION |
PanVerification | pan | PAN_VERIFICATION |
BankVerification | bank | BANK_VERIFICATION |
SelfieFaceMatch | face | FACE_MATCH |
Liveness | liveness | VIDEO_LIVENESS |
GeoPhotoInside, GeoPhotoOutside | none | DOCUMENT_PROCESSING |
DocumentUpload | none | DOCUMENT_PROCESSING |
BusinessDetails | pincode | none |
Consent | none | none |
There is no separate charge per request or per submission: the journey price covers them. Failures on the provider's side (timeouts and outages) are never charged. Provider calls are cached for ten minutes per identical input so a retry is not billed twice. A step that succeeds is locked: the applicant cannot go back and redo it.
MobileOtp
- Config
channel(sms),expirySeconds(300),maxAttempts(3)- Applicant sees
- Mobile number field (prefilled and locked if you passed
mobile), then a six-digit code input with resend timer (30 seconds) - Limits
- 5 sends per hour per step, 10 per hour per number per workspace, one active code per step
- Result
verified.mobile(masked, for example98XXXXXX34); step data holds the masked target- Errors
INVALID_MOBILE,INVALID_OTP,OTP_EXPIRED,OTP_ATTEMPTS_EXCEEDED- Flags
PROVIDER_UNAVAILABLEif SMS delivery times out (delivery is retried in the background)
EmailOtp
- Config
expirySeconds(600),maxAttempts(3)- Applicant sees
- Email field (prefilled and locked if you passed
email) and code input - Result
verified.email(masked)
PersonalDetails
- Config
minAge(18),fields(fullName,dob,gender)- Applicant sees
- Name, date of birth, gender (
M,F,O) - Result
verified.fullName,verified.dateOfBirth,verified.gender- Errors
INVALID_NAME,INVALID_DATE,UNDERAGE
Address
- Config
- none
- Applicant sees
- Address lines, pincode with automatic city and state lookup, city, state
- Result
verified.address- Errors
INVALID_PINCODE
DigiLockerAadhaar
- Config
- none
- Applicant sees
- A button that opens the DigiLocker consent page (a simulator in the sandbox), then returns and shows progress while VerifyMe polls the provider
- Result
verified.fullName,verified.dateOfBirth,verified.gender,verified.addressand maskedverified.aadhaar(last four digits only). The full Aadhaar number is never stored or returned- Events
verification.pendingthenverification.completed- Errors
DIGILOCKER_NOT_COMPLETED(denied or abandoned),PROVIDER_UNAVAILABLE
PanVerification
- Config
requireNameMatch(true),nameMatchMin(70),nameMismatch(blockorflag, defaultblock)- Applicant sees
- PAN field (uppercase, format
ABCDE1234F). The name to match comes from DigiLocker, then Personal details, then an optional entered name - Limits
- 10 attempts per hour per request
- Result
verified.pan(masked,ABXXXXXX4F); step data has category (Individual, Company, Firm, HUF, Trust, AOP)- Flags
- By default a name mismatch stops the step (retry, then
PAN_VERIFICATION_FAILED). WithnameMismatch: flagthe applicant continues andPAN_AADHAAR_MISMATCH(HIGH) if the PAN holder differs from the DigiLocker name (similarity below 70), elsePAN_NAME_MISMATCH(MEDIUM) against the entered name - Errors
INVALID_PAN,PAN_INVALID,PROVIDER_UNAVAILABLE(attempt not consumed)
BankVerification
- Config
nameMatchMin(70),nameMismatch(blockorflag, defaultblock)- Applicant sees
- Account number (9 to 18 digits), IFSC, optional holder name
- Limits
- 8 attempts per hour per request
- Result
verified.bankAccount(masked, last four digits),verified.bankAccountHolder- Name check
- The account holder must match the Aadhaar name or the PAN name (one is enough). Matching ignores word order, accepts initials, spelling variants and a missing middle name. A mismatch asks the applicant to retry; after the allowed attempts the step fails with
BANK_VERIFICATION_FAILEDand the applicant is told to contact the business. WithnameMismatch: flagthe applicant continues and aBANK_NAME_MISMATCHflag is raised for review - Errors
INVALID_ACCOUNT,INVALID_IFSC,BANK_ACCOUNT_INVALID,PROVIDER_UNAVAILABLE
SelfieFaceMatch
- Config
threshold(60, allowed 30 to 99),maxAttempts(3). Advanced:onFail: "block"fails the step permanently instead of sending to review;providerFallback: "autopass"completes the step with flagAUTOPASS_PROVIDER_FAILURE(HIGH) if the face provider is down- Applicant sees
- Camera capture (or file picker) with guidance, retake on mismatch with attempts left
- Result
verified.faceMatchScore,verified.faceMatchPassed; aSELFIEdocument- Flags
FACE_BELOW_THRESHOLD(HIGH) when attempts are exhausted. The applicant is told the selfie will be reviewed manually- Errors
FACE_MISMATCH(retry),FACE_VERIFICATION_FAILED(when blocked),FILE_TYPE_NOT_ALLOWED,FILE_TOO_LARGE
Liveness
- Config
maxAttempts(3)- Applicant sees
- Short guided capture of 3 to 10 frames
- Result
verified.livenessPassed; aLIVENESS_FRAMEdocument- Flags
LIVENESS_FAILED(HIGH) on failure (retryable)
GeoPhotoInside and GeoPhotoOutside
- Config
requireGeo(false)- Applicant sees
- Camera capture for the premises with a location permission prompt
- Result
- A document typed
PREMISES_INSIDEorPREMISES_OUTSIDEwith latitude, longitude and accuracy visible to reviewers. Not part of theverifiedobject - Flags
GEO_UNAVAILABLE(LOW) if no location was captured andrequireGeois false. WithrequireGeo: truethe photo is refused withGEO_REQUIRED
DocumentUpload
- Config
documentTypes(defaultREGISTRATION_CERTIFICATE,ADDRESS_PROOF),maxFiles(3)- Applicant sees
- Choose a document type, upload JPEG, PNG, WebP or PDF (up to 8 MB), then Done
- Result
- Entries in
documents[]of the result (type,contentType,sizeBytes,scanStatus,uploadedAt) and adocument.uploadedevent each - Flags
DOCUMENT_SCAN_FAILED(HIGH) for files rejected by the malware scan- Errors
MAX_FILES_REACHED,DOCUMENT_REQUIRED,FILE_TYPE_NOT_ALLOWED
BusinessDetails
- Config
fields(businessName,businessType,gstin,pincode,city,state)- Applicant sees
- Business name, type (
proprietorship,partnership,llp,private_limited,public_limited,other), optional GSTIN, location - Result
- Recorded on the profile (GSTIN stored masked) and shown to reviewers. In 1.0.0 these fields are not returned in the API result object
- Errors
INVALID_GSTIN,INVALID_PINCODE
Consent
- Config
- none (the notice is generated from the flow's
purposeCode, step purposes andnoticeVersion) - Applicant sees
- A clear notice naming your business, each purpose, and an I agree control
- Result
- One consent record per purpose with notice version, text hash, time, IP, locale and source (hosted or SDK). Withdrawal is available to the applicant
- Errors
CONSENT_REQUIRED
Flags and severities
A flag is a risk signal produced by a step. Flags never block an applicant by themselves; they inform automated and manual review. Flags are immutable per attempt, and the active set is whatever the latest attempt of each step produced, so a successful retry clears the earlier flag from the active set (history is kept for audit).
| Flag | Severity | Raised by | Meaning |
|---|---|---|---|
PAN_AADHAAR_MISMATCH | HIGH | PAN | PAN holder name differs from the DigiLocker name |
FACE_BELOW_THRESHOLD | HIGH | Selfie | Face score below threshold after all attempts |
LIVENESS_FAILED | HIGH | Liveness | Liveness check failed |
DOCUMENT_SCAN_FAILED | HIGH | Uploads, selfie | File rejected by malware scan |
AUTOPASS_PROVIDER_FAILURE | HIGH | Selfie | Face provider down and the flow allowed auto-pass. Never auto-approved |
PAN_NAME_MISMATCH | MEDIUM | PAN | PAN holder name differs from the entered name |
BANK_NAME_MISMATCH | MEDIUM | Bank | Account holder differs from the expected name |
GEO_UNAVAILABLE | LOW | Geo photo | No location captured |
PROVIDER_UNAVAILABLE | INFO | Any provider step | A provider timed out. Applicant could retry. Not a negative result |
The result API exposes flags[] as { code, step, severity }. Flags without a mapping default to LOW.
Review policies
After the applicant submits, the request is reviewed. The effective policy is decided like this:
| Policy | Behaviour |
|---|---|
MANUAL | Always goes to the review queue. Default for new tenants |
AUTO | Approved automatically when there is no HIGH active flag |
HYBRID | Approved automatically only when there is no active flag except INFO; otherwise a human decides |
- Tenant policy is a ceiling. If the workspace policy is
MANUAL, no flow is ever auto-approved, whatever the flow says. Set the workspace policy toAUTOorHYBRIDto let flows choose. - A request with
AUTOPASS_PROVIDER_FAILUREis never auto-approved. - Automatic approvals are recorded with reason code
VERIFIED_OK, comment "Auto-approved by ... review policy", anddata.automated: trueinonboarding.approved. Rejection is never automatic.
Reviewer actions
Reviewers work in the portal. Each action is audited and emits a webhook:
| Action | Result | Event |
|---|---|---|
start | Request moves to REVIEWING | none |
approve | APPROVED. All required steps must be complete | onboarding.approved |
reject | REJECTED | onboarding.rejected |
revert / request_info | REVERTED with the chosen steps set back for the applicant; link reopens, expiry extended | onboarding.reverted |
Reason codes
Approve, reject and revert require a reason code: VERIFIED_OK, DOCUMENT_UNCLEAR, DATA_MISMATCH, FACE_MISMATCH, MISSING_INFORMATION, FRAUD_SUSPECTED, POLICY_VIOLATION, OTHER. A workspace can also require a free-text comment on every decision (requireReasonComment). Comments are internal and are not sent in webhooks, except the applicant-facing message of a revert / request for information.
Maker-checker
With maker-checker enabled for the workspace, a final approve or reject needs two different reviewers. The first reviewer's decision is recorded as *prepared* and the request shows review status AWAITING_CHECKER; the same user cannot confirm it. Only the second reviewer's confirmation changes the status and emits the event. Automated policy approvals are exempt.