KYC
Verify a user's identity so they can withdraw INR, gift assets or take physical delivery. Email, an instant PAN check, then Aadhaar by DigiLocker or by photos. All capability `kyc`.
KYC is required before a user can withdraw, gift or take physical delivery. Buying, selling and *receiving* gifts do not require it.
POST kyc/aadhaar/otp/send and kyc/aadhaar/otp/verify now answer 410 AADHAAR_OTP_RETIRED, in live and test mode. Keep your email and PAN steps, and replace the OTP screens with DigiLocker (instant) or Aadhaar photos (reviewed). The PAN step now answers nextStep: "aadhaar".The flow
Every call is under /partner/users/:user_code/kyc/. Always start by reading the status — it tells you the exact next step, so the flow resumes cleanly if a user drops off.
- Your server:
Verify the email
Send a 6-digit code to the user's email and pass back what they type.
POST kyc/email /send · kyc /email /verify - Your server:
Check the PAN
PAN, date of birth and name as on the card. Answered instantly.
POST kyc/pan /verify A PAN photo is the fallback, only after an outage or two failed checks.
- The user:
Aadhaar by DigiLocker
The user signs in to DigiLocker and taps Allow. Instant when everything matches.
POST kyc/aadhaar /digilocker Or send Aadhaar photos instead: kyc/aadhaar/photos.
- OroPocket:
Approve or review
Instant approval, or OroPocket's review — usually within 24–48 h.
- OroPocket:
We call your webhook
kyc.approved, kyc.under_review or kyc.rejected lands on your endpoint.
When a user is approved
| Path | Outcome |
|---|---|
| Instant PAN + DigiLocker | Approved in seconds when the name and date of birth match across PAN and Aadhaar. kyc.approved fires. |
| Anything else | A PAN photo, Aadhaar photos, a name or date-of-birth mismatch, or a possible duplicate goes to OroPocket's review (kyc.under_review), usually within 24–48 h, then kyc.approved or kyc.rejected. |
Check status
/partner/users/:user_code/kyc/statuskycDrive your UI off v2.nextStep. It is the one field you need; the rest is detail.
| Field | Type | Description |
|---|---|---|
| status | string | not_started | email_pending | email_verified | details_pending | documents_pending | under_review | approved | rejected. |
| emailVerified | boolean | Stage 1 done. |
| rejectionReason | string | null | The reviewer's message after a rejection. |
| flow | string | "v2". |
| v2.nextStep | string | null | email_verification | pan | aadhaar | awaiting_review, or null once approved. |
| v2.panVerified | boolean | The instant PAN check passed. |
| v2.panMethod | string | null | instant | photo. |
| v2.panPhotoAllowed | boolean | True when the PAN photo fallback is open for this user. |
| v2.aadhaarVerified | boolean | Aadhaar is verified. |
| v2.aadhaarMethod | string | null | digilocker | photos | instant (verified earlier by the retired Aadhaar OTP). |
| v2.aadhaarLast4 | string | null | For display. |
| v2.digilocker | object | null | The latest DigiLocker session: {sessionId, status, failureCode}. |
| v2.nextStep | Show the user |
|---|---|
| email_verification | The email screen (Stage 1). |
| pan | The PAN screen (Stage 2). |
| aadhaar | A choice: DigiLocker (Stage 3a) or Aadhaar photos (Stage 3b). |
| awaiting_review | “We're checking your documents.” Wait for kyc.approved or kyc.rejected. |
| null | Done — status is approved. |
curl https://api.oropocket.com/partner/users/usr_xxx/kyc/status \
-H "Authorization: Bearer oro_live_xxx"Test mode returns the same shape, plus simulated: true.
Stage 1 — email
/partner/users/:user_code/kyc/email/sendkyc| Field | Type | Description | |
|---|---|---|---|
| string | required | The user's permanent email. Must not already belong to another OroPocket account; disposable addresses are refused. |
| Field | Type | Description |
|---|---|---|
| user_code | string | The user. |
| string | Where the code went. | |
| expiresAt | string | ISO time the code stops working (30 minutes). |
/partner/users/:user_code/kyc/email/verifykyc| Field | Type | Description | |
|---|---|---|---|
| verification_code | string | required | The 6-digit code (alias: "code"). 123456 in test mode. |
| Field | Type | Description |
|---|---|---|
| user_code | string | The user. |
| string | The verified email. | |
| verifiedAt | string | ISO time it was verified. Next: the PAN. |
Failures come back as errors, not as success: true — for example 400 DISPOSABLE_EMAIL, or 400 INVALID_CODE for a wrong or expired code. Show error.message to the user.
Stage 2 — PAN
/partner/users/:user_code/kyc/pan/verifykycAn instant check against the income-tax record. The email must be verified first.
| Field | Type | Description | |
|---|---|---|---|
| pan | string | required | Individual PAN, format AAAAA9999A. |
| dateOfBirth | string | required | YYYY-MM-DD. Must match the PAN record; the user must be 18 or older. |
| nameAsPerPan | string | required | The name exactly as printed on the PAN card. |
| Field | Type | Description |
|---|---|---|
| panVerified | boolean | true. |
| kycStatus | string | The user's KYC status after the check. |
| nextStep | string | "aadhaar". |
curl -X POST https://api.oropocket.com/partner/users/usr_xxx/kyc/pan/verify \
-H "Authorization: Bearer oro_live_xxx" \
-H "Content-Type: application/json" \
-d '{"pan":"ABCPK1234Z","dateOfBirth":"1990-08-15","nameAsPerPan":"ASHA KUMARI"}'| Error code | Meaning |
|---|---|
| 400 INVALID_INPUT | error.details.field says which field: pan, dateOfBirth or nameAsPerPan. Bad format, PAN not valid or inactive, not an individual PAN, name or date of birth doesn't match, under 18, or the PAN is already registered to another account. Show error.message beside that field. |
| 400 EMAIL_NOT_VERIFIED | Do Stage 1 first. |
| 409 UNDER_REVIEW · ALREADY_APPROVED | Nothing to do — the KYC is with our review, or already approved. |
| 429 RATE_LIMITED | 5 PAN checks per user per day. The PAN photo route opens after two failed checks. |
| 503 PROVIDER_UNAVAILABLE | The verification service is briefly unavailable. Retry later, or use the PAN photo. |
Stage 2b — PAN photo (fallback only)
/partner/users/:user_code/kyc/pan/photokycOnly open when v2.panPhotoAllowed is true: after the instant check hit a service outage, or the user failed it twice. Otherwise it answers 400 PAN_PHOTO_NOT_ALLOWED. A PAN photo always means the KYC ends in OroPocket's review.
| Field | Type | Description | |
|---|---|---|---|
| pan | string | required | AAAAA9999A. |
| dateOfBirth | string | required | YYYY-MM-DD. |
| nameAsPerPan | string | required | As printed on the card. |
| image | string | required | Photo of the PAN card: base64 JPEG, PNG or PDF, up to 4 MB. |
| Field | Type | Description |
|---|---|---|
| panMethod | string | "photo". |
| nextStep | string | "aadhaar". |
Stage 3a — Aadhaar by DigiLocker (recommended)
/partner/users/:user_code/kyc/aadhaar/digilockerkycDigiLocker is the Government of India's document wallet. The user signs in there (mobile number or Aadhaar, plus an OTP from DigiLocker) and taps Allow to share their Aadhaar with OroPocket. The PAN step must be done first.
| Field | Type | Description | |
|---|---|---|---|
| returnUrl | string | optional | Where to send the user's browser afterwards. https, on an origin you registered (see below). |
| Field | Type | Description |
|---|---|---|
| sessionId | string | Keep it — you read the outcome with it. |
| url | string | An OroPocket link (https://api.oropocket.com/kyc/digilocker/go/…) that forwards to DigiLocker. Open it for the user. |
| expiresAt | string | ISO time. The link is valid for about 15 minutes. |
Calling it again within about 12 minutes returns the same session, so a double tap doesn't start two.
Opening the url. Use one of these:
| Where | How |
|---|---|
| Phone browser | Hand the url to the system browser. Simplest; your returnUrl brings the user back. |
| In-app browser | Chrome Custom Tabs on Android, SFSafariViewController on iOS. Recommended for apps. |
| Full-screen WebView | Load the url as the top-level page of a WebView, with JavaScript and DOM storage on. |
| Web app | Navigate the same tab (window.location.assign(url)) or open a new tab. |
returnUrl. When the user finishes, the browser goes to your returnUrl with ?session_id=<id>&status=<status> appended. Treat status there as a hint only — confirm with the GET below or the webhook before you show success. The origin must be listed in your panel under Payments → Checkout → Return origins (the same list payment returns use; if you don't see Payments in your panel, ask your OroPocket contact to add the origin). Without a returnUrl, the user sees "DigiLocker step complete — go back to the app", which suits an in-app browser you close yourself.
/partner/users/:user_code/kyc/aadhaar/digilocker/:session_idkyc| Field | Type | Description |
|---|---|---|
| sessionId | string | The session. |
| status | string | See the table below. |
| failureCode | string | null | Why the session ended badly — see the codes below. null while pending or after success. |
| kycStatus | string | null | The user's overall KYC status once the Aadhaar was checked; null while pending, and when the session ended before that (denied, expired, a DigiLocker failure). |
| status | What to do |
|---|---|
| pending | The user hasn't finished yet. Keep waiting. |
| verified | Aadhaar verified and KYC approved. Done. |
| review | Aadhaar verified; a person checks the rest. Show “in review” and wait for kyc.approved or kyc.rejected. |
| denied | The user didn't tap Allow. Start a new session if they want to try again. |
| expired | Not finished in time. Start a new session. |
| not_linked | That DigiLocker account has no Aadhaar. Offer Aadhaar photos (Stage 3b). |
| failed | See failureCode. provider_unavailable: try again later or use photos. under_18 and duplicate_aadhaar can't be fixed by retrying. |
| failureCode | Meaning |
|---|---|
| consent_denied | With status denied: the user didn't tap Allow. |
| link_expired | With status expired: the user didn't finish in time. |
| never_started | With status expired: the link was never opened. |
| status_unavailable | With status expired: we couldn't confirm the result before the session ran out. Start again. |
| aadhaar_not_linked | With status not_linked: no Aadhaar in that DigiLocker account. Use photos. |
| document_unreadable | The Aadhaar from DigiLocker couldn't be read. Start again or use photos. |
| provider_unavailable | DigiLocker or our verification service was down. Try later or use photos. |
| provider_failed | DigiLocker reported a failure. Start again or use photos. |
| under_18 | The Aadhaar shows the user is under 18. KYC can't continue. |
| duplicate_aadhaar | That Aadhaar already belongs to another OroPocket account. |
Polling or webhooks? Webhooks are the reliable signal: kyc.approved or kyc.under_review fires when the session ends well. If you want an immediate answer when the user lands on your returnUrl (or closes the in-app browser), call the GET once, then every 3 seconds or so while it says pending — and stop after a couple of minutes.
# 1) start a session
curl -X POST https://api.oropocket.com/partner/users/usr_xxx/kyc/aadhaar/digilocker \
-H "Authorization: Bearer oro_live_xxx" \
-H "Content-Type: application/json" \
-d '{"returnUrl":"https://app.example.com/kyc/done"}'
# 2) after the user comes back, read the outcome
curl https://api.oropocket.com/partner/users/usr_xxx/kyc/aadhaar/digilocker/9b2c4e7a1f0d5c38e6a4b1d27f90c3e5 \
-H "Authorization: Bearer oro_live_xxx"| Error code | Meaning |
|---|---|
| 400 PAN_REQUIRED | Do Stage 2 first. |
| 400 INVALID_RETURN_URL | returnUrl isn't a valid https URL. |
| 400 RETURN_URL_NOT_ALLOWED | returnUrl's origin isn't in Payments → Checkout → Return origins. |
| 403 DIGILOCKER_OFF | DigiLocker is switched off for your account. Use Aadhaar photos. |
| 409 ALREADY_VERIFIED · ALREADY_APPROVED · UNDER_REVIEW | Aadhaar is already verified, the KYC is approved, or it's with our review. |
| 429 RATE_LIMITED | 10 DigiLocker opens per user per hour. Offer photos, or try later. |
| 503 PROVIDER_UNAVAILABLE | The verification service is briefly unavailable. Retry later, or use Aadhaar photos. |
| 404 SESSION_NOT_FOUND | GET only: that session id doesn't exist for this user. |
Stage 3b — Aadhaar photos
/partner/users/:user_code/kyc/aadhaar/photoskycFor users without DigiLocker, or when it isn't working. The KYC goes to OroPocket's review (usually within 24–48 h) and you hear back by kyc.approved or kyc.rejected.
| Field | Type | Description | |
|---|---|---|---|
| aadhaarNumber | string | required | 12 digits. |
| front | string | required | Front of the Aadhaar card: base64 JPEG, PNG or PDF, up to 4 MB. |
| back | string | required | Back of the Aadhaar card, same rules. |
| Field | Type | Description |
|---|---|---|
| kycStatus | string | "under_review". |
| aadhaarMethod | string | "photos". |
data:image/jpeg;base64, prefix. JPEG, PNG or PDF, up to 4 MB each before encoding. Remember base64 grows the body by about a third.curl -X POST https://api.oropocket.com/partner/users/usr_xxx/kyc/aadhaar/photos \
-H "Authorization: Bearer oro_live_xxx" \
-H "Content-Type: application/json" \
-d "{\"aadhaarNumber\":\"234567890124\",\"front\":\"$(base64 -w0 front.jpg)\",\"back\":\"$(base64 -w0 back.jpg)\"}"| Error code | Meaning |
|---|---|
| 400 PAN_REQUIRED | Do Stage 2 first. |
| 400 INVALID_INPUT | error.details.field is aadhaarNumber: not a valid 12-digit number, or already registered to another account. |
| 400 INVALID_DOCUMENT · DOCUMENT_TOO_LARGE | An image isn't a readable JPEG, PNG or PDF, or is over 4 MB. |
| 409 UNDER_REVIEW · ALREADY_APPROVED | Already with our review, or already approved. |
After a rejection
kyc.rejected carries the reviewer's message in data.reason (also in rejectionReason on the status). The user can start again: read the status and follow v2.nextStep. Webhook payloads and the kyc.under_review reason codes are on Webhooks.
The older manual path
POST kyc/details (fullName, aadharNumber, panNumber, dateOfBirth, optional address) followed by POST kyc/documents (publicly fetchable panImageUrl, aadharFrontImageUrl, aadharBackImageUrl) still works and ends in review, but it is no longer recommended — use the PAN and Aadhaar steps above. If you already hold a user's verified PAN and Aadhaar record, see Submit collected KYC.
Testing it
With an oro_test_ key nothing real is checked and no real documents are needed.
| Test-mode value | Result |
|---|---|
| Email code 123456 | Verifies. |
| PAN ABCDE1234F | Verifies. Any other well-formed PAN verifies too; date of birth and name aren't checked. |
| PAN ZZZZZ9999Z | Rejected with 400 KYC_REJECTED — use it to test your failure path. |
| kyc/pan/photo | Always accepted in test mode, so you can build the fallback screen. |
| kyc/aadhaar/digilocker | Returns a url to a DigiLocker simulator (https://api.oropocket.com/partner/sandbox/digilocker/…) with Allow and Deny. Allow approves the KYC and sends kyc.approved; Deny leaves the session denied. Both redirect to your returnUrl, as live does. |
| kyc/aadhaar/photos | Any 12-digit number starting 2–9 and any base64 front and back. Goes to under_review (kyc.under_review), then approves itself about 30 seconds later (kyc.approved). |
| kyc/aadhaar/otp/* | 410 AADHAAR_OTP_RETIRED, as in live. There is no Aadhaar OTP magic value. |
Webhooks fire in test mode too, with data.mode: "sandbox". Try every call from the Playground; more on test mode in Sandbox.