1. Stage 2 Objective
Customer verifies the 4-digit OTP delivered via SMS. After successful verification, the platform fires background checks asynchronously — the customer does not wait and immediately proceeds to Stage 3.
Background Check Pipeline (Async)
The checks execute in a strict dependency chain: Zintlr → (Hyperverge + C-safe in parallel) → (NSDL + CVL KRA in parallel). UTI is a backup only — it is called exclusively when NSDL is DOWN.
Key Principle
OTP verification is synchronous and blocking. All background checks (PAN lookup, KRA, C-safe) are asynchronous and non-blocking. The customer never waits for background checks to complete.
2. Preconditions
| Condition | Detail |
lead.state = INITIATED | Lead must have been created in Stage 1 with state INITIATED |
| Valid OTP in server memory (in-memory store keyed by mobile number) | OTP must exist in in-memory OTP store with TTL of 5 minutes, keyed by mobile number |
| Active session from Stage 1 | Session token issued during Stage 1 registration must still be valid |
3. Backend Flow — OTP Verification
1
Validate OTP against in-memory OTP store for mobile number. Retrieve the stored OTP from in-memory store using the mobile number as key. Compare the submitted 4-digit OTP with the stored value.
2
OTP correct → Set lead.state = OTP_VERIFIED. Record mobile_verified_at timestamp. Customer moves to Stage 3 immediately. Remove OTP from in-memory store.
3
OTP incorrect → Increment attempt_count in in-memory store. Return error response with remaining attempts (e.g., "3 attempts remaining"). Do not change lead state.
4
5th wrong attempt → OTP locked. Set lead.state = DROPPED with reason DROP_OTP_LOCKED. Remove OTP from in-memory store. Journey is terminated.
5
OTP expired → Allow resend. Maximum 3 resends in a 30-minute window with a 30-second cooldown between resends. Each resend generates a new 4-digit OTP and resets the 5-minute TTL.
6
On success: Fire background checks asynchronously. After setting OTP_VERIFIED, dispatch the background check pipeline via async job queue. The customer does NOT wait for these to complete.
Critical: OTP Lock is Permanent
Once a lead reaches 5 wrong OTP attempts, the lead is DROPPED with DROP_OTP_LOCKED. There is no unlock mechanism. The customer must restart the entire journey with a new lead.
4. Background Check Pipeline (Async, After OTP Verified)
All background checks run asynchronously after OTP verification succeeds. They follow a strict dependency chain:
Step 1: Zintlr ONLY (needs plain mobile number)
|
v output = PAN number
Step 2: Hyperverge + C-safe in PARALLEL (both need PAN)
|
v Hyperverge output = Name + DOB
Step 3: NSDL + CVL KRA in PARALLEL
|
v If NSDL returns non-success OR throws exception --> UTI fallback
UTI is backup ONLY, never called if NSDL succeeds
Step 1: Zintlr — Phone to PAN
1
Call Zintlr API with the customer's plain mobile number. Zintlr returns the PAN number associated with that mobile. If Zintlr fails or returns no PAN, the pipeline continues — PAN will be collected manually at Stage 4.
Step 2: Hyperverge + C-safe (Parallel)
Both calls require the PAN from Step 1. If Step 1 returned no PAN, these calls are skipped.
2a
Hyperverge — Call with PAN. Returns pan_name and pan_dob. These are stored for pre-fill at Stage 4.
2b
C-safe — Call with PAN. Returns PEP/sanctions screening result. Stored as csafe_result. Non-blocking for the customer journey.
Step 3: NSDL + CVL KRA (Parallel)
Both calls run in parallel. NSDL validates the PAN. CVL KRA checks the KRA registration status.
3a
NSDL / Protean — Validate PAN against NSDL database. Store result as nsdl_pan_valid = true/false. If NSDL returns non-success OR throws an exception, trigger UTI fallback.
3b
CVL KRA — Check KRA registration status. Returns a status code (007, 001, 000, 006, 999, etc.) mapped to kra_status_pan_stage.
UTI Fallback Rule
UTI (PanAPI.GetPanDetails_UTI()) is a backup provider. It is called only when NSDL returns a non-success response or throws an exception. UTI is never called if NSDL succeeds. This is not a parallel call — it is a conditional fallback.
5. Backend Validations
| Check | Condition | Result | Blocking? |
| OTP match |
Submitted OTP matches in-memory stored OTP for mobile number |
Proceed to Stage 3; fire background checks |
Yes — synchronous gate |
| OTP expiry |
OTP key does not exist in in-memory store (TTL expired) |
Return error FE_OTP_002; prompt resend |
Yes — must resend |
| OTP lock (5 wrong attempts) |
attempt_count >= 5 |
lead.state = DROPPED, reason = DROP_OTP_LOCKED |
Yes — journey terminated |
| KRA RESTRICTED |
CVL KRA returns status code 006 |
Stored as kra_status_pan_stage = RESTRICTED |
No — deferred to Stage 4 |
| KRA 999 (INVALID_PAN) |
CVL KRA returns status code 999 |
Stored as kra_status_pan_stage = INVALID_PAN |
No — deferred to Stage 4 |
| All background checks fail |
Zintlr, Hyperverge, NSDL, CVL KRA all return errors |
Log failures; customer proceeds to Stage 3 unaffected. PAN collected manually at Stage 4. |
No — non-blocking |
Deferred Validations
KRA RESTRICTED (006) and KRA INVALID_PAN (999) results are stored at Stage 2 but acted upon at Stage 4. The customer is not blocked or notified at this stage.
6. OTP Specification
| Parameter | Value | Notes |
| Length |
4 digits |
Numeric only (0000-9999) |
| Validity |
5 minutes |
TTL enforced by in-memory store with background cleanup |
| Max wrong attempts |
5 |
5th wrong attempt → lead.state = DROPPED, DROP_OTP_LOCKED |
| Max resend |
3 per 30-minute window |
Tracked via in-memory counter with 30-min TTL |
| Cooldown |
30 seconds |
Minimum interval between consecutive resend requests |
| Storage |
In-memory only |
OTP is never persisted to the database. In-memory store keyed by mobile number + OTP type |
| Delivery |
SMS cascade |
Primary SMS provider with fallback cascade if delivery fails |
7. Background Check Decision Tables
7.1 PAN Pre-fetch Permutation Table
All combinations of Zintlr, Hyperverge, and NSDL results and their outcomes:
| # | Zintlr | Hyperverge | NSDL | Outcome |
| 1 |
Found (PAN returned) |
Found (Name + DOB) |
Valid |
All stored. pan_number, pan_name, pan_dob, nsdl_pan_valid = true. Pre-fill at Stage 4. |
| 2 |
Found (PAN returned) |
Found (Name + DOB) |
Invalid |
PAN stored. nsdl_pan_valid = false. Name/DOB from Hyperverge stored but PAN flagged for re-validation. |
| 3 |
Found (PAN returned) |
Failed / No data |
Valid |
PAN stored and validated. pan_name and pan_dob not available — customer enters manually at Stage 4. |
| 4 |
Found (PAN returned) |
Failed / No data |
Invalid |
PAN stored but nsdl_pan_valid = false. No Hyperverge data. Customer enters PAN details manually at Stage 4. |
| 5 |
Found (PAN returned) |
Failed / No data |
Failed / Down |
PAN stored. NSDL failed → trigger UTI fallback. If UTI also fails, nsdl_pan_valid = null. Deferred validation at Stage 4. |
| 6 |
Not found / Failed |
Skipped (no PAN) |
Skipped (no PAN) |
No PAN available. All downstream checks skipped. Customer enters PAN manually at Stage 4. Full validation runs at that point. |
| 7 |
Found (PAN returned) |
Found (Name + DOB) |
Failed / Down |
PAN and Hyperverge data stored. NSDL failed → trigger UTI fallback. If UTI succeeds, nsdl_pan_valid set from UTI result. |
Pipeline Rule
If Zintlr does not return a PAN (row 6), all downstream checks (Hyperverge, C-safe, NSDL, CVL KRA) are skipped. The pipeline terminates gracefully and PAN is collected manually at Stage 4.
7.2 KRA Status Results
CVL KRA API response codes mapped to internal status values:
| KRA Raw Code | Internal Status | Description | Action |
007 series |
KRA_VALIDATED |
Customer is KRA registered and validated |
Store status. Pre-fill KRA data (email, name) at Stage 4. |
001, 002, etc. |
KRA_MOD |
Customer has KRA record but with modifications pending |
Store status. Allow journey to continue. Handle at Stage 4. |
000, 003, etc. |
NON_KRA |
Customer is not KRA registered |
Store status. Fresh KRA registration required during journey. |
006 |
RESTRICTED |
Customer is restricted / blacklisted in KRA |
Store as DROP_KRA_RESTRICTED. Deferred — acted upon at Stage 4. |
999 |
INVALID_PAN |
PAN not recognized by KRA system |
Store status. Deferred — re-validation at Stage 4. |
| API timeout / error |
API_DOWN |
CVL KRA API is unreachable or timed out |
Log failure. Store kra_status_pan_stage = API_DOWN. Non-blocking. |
8. Existing Stored Procedure Mapping
8.1 Stored Procedures (Old System)
These stored procedures from the legacy system map to Stage 2 functionality. They serve as reference for what the new backend must replicate:
| Stored Procedure | Purpose | New Backend Equivalent |
RESUME_AUTHENTICATE_USER_OTP_PASS |
OTP validation — authenticates user after successful OTP entry |
OTP verification service + in-memory OTP store lookup |
USP_VALIDATE_UPDATE_OTP_MOBILE_SJET |
Validate OTP and update mobile verification status in DB |
In-memory OTP check + leads.mobile_verified_at update |
USP_INSERT_KRA_LOGS_SJET |
Insert KRA API request/response logs |
kra_records table insert + structured logging |
USP_CALL_KRAAPI_ONSUBMIT_SJET |
KRA API call decision logic — determines whether to call KRA |
Background pipeline orchestrator (Step 3) |
USP_INSERT_CSAFE_Req_Resp |
Insert C-safe API request/response logs |
csafe_checks table insert + structured logging |
USP_GETCSAFEFLAG_SJET |
Retrieve C-safe configuration flags (enabled/disabled) |
provider_configurations table lookup |
USP_INSERT_UPDATE_CSAFE_PEP_SJET |
Insert or update PEP (Politically Exposed Person) flag from C-safe |
csafe_checks.pep_flag update |
USP_INSERT_ZINTLR_PANAPI_DETAILS_SJET |
Log Zintlr phone-to-PAN API request/response details |
pan_verifications table insert (source = zintlr) |
USP_PANVALIDATION_REQUEST_RESPONSE_LOG_SJET |
Log PAN validation request/response (NSDL/UTI) |
pan_verifications table insert (source = nsdl/uti) |
CHECK_STAFF_PAN |
Check if the PAN belongs to a staff member |
Staff PAN lookup in pan_verifications or dedicated staff table |
USP_CHECK_NSDLRESP |
Check and parse NSDL response for PAN validation result |
NSDL response parser in PAN verification service |
8.2 Old External API Calls
Legacy API methods that the new backend must integrate with (or replace):
| Old API Method | Vendor | Purpose | Pipeline Step |
PanAPI.CallMobileToPanDetailsAsync() |
Zintlr |
Phone-to-PAN lookup — takes mobile number, returns PAN |
Step 1 |
PanAPI.GetPanDetailsHyperverge() |
Hyperverge |
PAN details lookup — takes PAN, returns Name + DOB |
Step 2a |
PanAPI.VerifyPAN_NSDL() |
NSDL / Protean |
PAN validation against NSDL database |
Step 3a |
PanAPI.GetPanDetails_UTI() |
UTI |
UTI fallback PAN validation — called only when NSDL is down |
Step 3a (fallback) |
Migration Note
The old system stored OTPs in the database (USP_VALIDATE_UPDATE_OTP_MOBILE_SJET). The new system stores OTPs exclusively in server memory (in-memory ConcurrentDictionary keyed by mobile number). No OTP values should be persisted to any database table.
9. Error / Drop Codes
| Code | Type | Trigger | User Impact |
FE_OTP_001 |
Frontend Error |
OTP entered is incorrect (attempts remaining) |
Show error with remaining attempt count. Customer can retry. |
FE_OTP_002 |
Frontend Error |
OTP has expired (in-memory store TTL elapsed) |
Prompt customer to request a new OTP via resend. |
DROP_OTP_LOCKED |
Drop |
5th incorrect OTP attempt |
lead.state = DROPPED. Journey terminated. Customer must restart. |
BE_OTP_002 |
Backend Error |
OTP resend limit exceeded (3 resends in 30 min) or cooldown not elapsed (30 sec) |
Return error. Customer must wait before requesting another resend. |
DROP_KRA_RESTRICTED |
Deferred Drop |
CVL KRA returns status code 006 (RESTRICTED) |
Stored at Stage 2 but acted upon at Stage 4. Customer is not notified at this stage. |
DROP_KRA_RESTRICTED is Deferred
Even though the KRA RESTRICTED status is discovered during Stage 2 background checks, the drop action is deferred to Stage 4. This is because Stage 2 background checks are async and the customer has already moved to Stage 3.
10. Exit State
After Stage 2 completes (OTP verified + background checks dispatched), the following fields are populated:
| Field | Value / Source | Notes |
lead.state |
OTP_VERIFIED |
Set synchronously upon OTP match. This is the immediate exit state. |
mobile_verified_at |
Timestamp (UTC) |
Recorded when OTP is verified successfully |
kra_status_pan_stage |
KRA_VALIDATED | KRA_MOD | NON_KRA | RESTRICTED | INVALID_PAN | API_DOWN |
Populated async by CVL KRA background check |
kra_raw_code |
007, 001, 000, 006, 999, etc. |
Raw response code from CVL KRA API |
csafe_result |
PEP/sanctions screening result |
Populated async by C-safe background check |
pan_number |
10-character PAN (e.g., ABCDE1234F) |
Populated async by Zintlr. May be null if Zintlr fails. |
pan_name |
Full name as per PAN card |
Populated async by Hyperverge. May be null if Hyperverge fails. |
pan_dob |
Date of birth as per PAN card |
Populated async by Hyperverge. May be null if Hyperverge fails. |
nsdl_pan_valid |
true | false | null |
Populated async by NSDL (or UTI fallback). null if both fail. |
kra_prefill_email |
Email from KRA record |
Available when kra_status_pan_stage = KRA_VALIDATED. Used for pre-fill at Stage 4. |
kra_prefill_name |
Name from KRA record |
Available when kra_status_pan_stage = KRA_VALIDATED. Used for pre-fill at Stage 4. |
Async Fields
All fields except lead.state and mobile_verified_at are populated asynchronously. They may still be null when the customer reaches Stage 3 or early Stage 4. The frontend must handle partial data gracefully.
11. Vendor & Integration Calls
| Vendor / System | Purpose | When Called | Sync / Async |
| In-Memory OTP Store |
OTP storage, validation, attempt tracking, resend counters |
OTP submit, OTP resend |
Sync |
| Zintlr |
Phone-to-PAN lookup |
After OTP verified (pipeline Step 1) |
Async |
| Hyperverge |
PAN-to-Name/DOB lookup |
After Zintlr returns PAN (pipeline Step 2a) |
Async |
| NSDL / Protean |
PAN validation |
After Zintlr returns PAN (pipeline Step 3a) |
Async |
| UTI (backup only) |
PAN validation fallback |
Only when NSDL returns non-success or throws exception |
Async |
| CVL KRA |
KRA registration status check |
After Zintlr returns PAN (pipeline Step 3b) |
Async |
| C-safe |
PEP / sanctions screening |
After Zintlr returns PAN (pipeline Step 2b) |
Async |
| CleverTap |
Event tracking (OTP verified, background check results) |
After OTP verified; after each background check completes |
Async |
| Zoho CRM |
Lead status update |
After OTP verified; after background checks complete |
Async |
| Datalake |
Analytics event logging |
After OTP verified; after each background check completes |
Async |
| CDP |
Customer data platform update |
After background check results are stored |
Async |
12. Database Tables (New Backend)
12.1 Operational Tables
| Table | Purpose | Key Fields (Stage 2 Relevant) |
leads |
Master lead record |
state (OTP_VERIFIED / DROPPED), mobile_verified_at, pan_number, pan_name, pan_dob, nsdl_pan_valid, kra_prefill_email, kra_prefill_name |
otp_verifications |
OTP verification event log |
lead_id, attempt_count, verified_at, status (VERIFIED / LOCKED / EXPIRED), resend_count |
pan_verifications |
PAN lookup/validation results from all providers |
lead_id, source (zintlr / hyperverge / nsdl / uti), request_payload, response_payload, pan_found, pan_valid, created_at |
kra_records |
CVL KRA check results |
lead_id, kra_raw_code, kra_status (KRA_VALIDATED / KRA_MOD / NON_KRA / RESTRICTED / INVALID_PAN / API_DOWN), kra_email, kra_name, request_payload, response_payload |
csafe_checks |
C-safe PEP/sanctions screening results |
lead_id, csafe_result, pep_flag, request_payload, response_payload, created_at |
journey_stage_events |
Stage transition audit trail |
lead_id, stage (STAGE_2), event (OTP_VERIFIED / OTP_FAILED / OTP_LOCKED), metadata, created_at |
downstream_events |
Async events dispatched to external systems |
lead_id, target (clevertap / zoho / datalake / cdp), event_type, status, dispatched_at |
lead_state_transitions |
State machine audit log |
lead_id, from_state (INITIATED), to_state (OTP_VERIFIED / DROPPED), reason, triggered_by, created_at |
12.2 Reference Tables
| Table | Purpose | Key Fields |
cvl_status_mapping |
Maps CVL KRA raw response codes to internal status values |
raw_code, internal_status (KRA_VALIDATED / KRA_MOD / NON_KRA / RESTRICTED / INVALID_PAN), description, is_active |
nsdl_error_master |
NSDL error code reference for PAN validation responses |
error_code, error_description, triggers_uti_fallback, is_active |
provider_configurations |
Feature flags and configuration for external providers |
provider_name (zintlr / hyperverge / nsdl / uti / cvl_kra / csafe), is_enabled, timeout_ms, retry_count, fallback_provider |
In-Memory OTP Store Keys (Not in DB)
OTP data lives exclusively in server memory (ConcurrentDictionary keyed by mobile number). Key patterns: mobile number + OTP type (OTP value, TTL 5 min), mobile number + attempts (wrong attempt counter), mobile number + resend (resend counter, TTL 30 min), mobile number + cooldown (cooldown flag, TTL 30 sec). Background cleanup removes expired entries.