compliance.verify_kyc — Full Intent Specification
INTENT NAMESPACE: compliance
INTENT NAME: verify_kyc
FULL ID: compliance.verify_kyc
VERSION: v1.0.0
STATUS: live
TTBS WEIGHTS: time 0.30 · taste 0.05 · budget 0.20 · safety 0.45
LAST UPDATED: 2026-05-10
Cross-cutting utility intent. Verifies user identity documents (Aadhaar, PAN, passport, driving license, voter ID) against authoritative sources. Invoked from inside other intents that require KYC (finance.apply_personal_loan, finance.buy_term_insurance, mobility.book_self_drive on first booking, etc.). Safety weight dominates.
1. NATURAL LANGUAGE COVERAGE
Classifies IN
- "verify my KYC for the loan"
- "do KYC for car rental"
- "complete KYC"
- "Aadhaar verification"
- "PAN verification"
- "DigiLocker KYC"
- "video KYC for insurance"
Classifies OUT — borderline NO
- "verify RC for buying used car" →
compliance.verify_rc_dl - "what's my KYC status" (read-only) → utility lookup
- "register my business with GSTIN" → not a KYC intent
MULTI-INTENT TRIGGERS (most common case)
KYC is almost always runtime-injected by another intent:
finance.apply_personal_loan→ injectedcompliance.verify_kycif user not already KYC'dmobility.book_self_drive→ injected if first-time rentermarketplace.buy_used_car→ injected for buyer + sellerfinance.buy_term_insurance→ injected if claim amount > ₹50L
Standalone invocation is rare but supported (user proactively does KYC before applying for products).
2. INPUT — TOMO → PROVIDER
{
"intent": "compliance.verify_kyc",
"intent_version": "v1.0.0",
"request_id": "req_kyc_8f4k_2026-05-10T08:14:00Z",
"user_session_id": "anon_user_token_or_uid",
"kyc_kind": "video_kyc",
"kyc_purpose": "personal_loan",
"regulator": "rbi",
"documents_to_verify": [
{
"doc_kind": "aadhaar",
"doc_number_masked": "•••• •••• 1234",
"verification_method": "digilocker"
},
{
"doc_kind": "pan",
"doc_number_masked": "•••• 1234X",
"verification_method": "income_tax_api"
}
],
"user_profile_provided": {
"full_name_legal": "REDACTED at TOMO ingest — partner gets via secure channel",
"date_of_birth": "REDACTED",
"gender": "REDACTED",
"address_line_1": "REDACTED",
"city": "REDACTED",
"state": "REDACTED",
"pincode": "REDACTED",
"country_code": "IN",
"phone_masked": "+91 98XXX XX234",
"email_masked": "k••••@gmail.com"
},
"is_runtime_injected": true,
"injected_from_intent": "finance.apply_personal_loan",
"injected_request_id": "req_pl_8f4k_...",
"minimum_kyc_level_required": "full_kyc",
"context": {
"user_locale": "en-IN",
"user_currency_pref": "INR",
"trust_signals": {
"is_repeat_kyc": false,
"prior_kyc_with_partner": 0,
"user_account_age_days": 312,
"previously_flagged": false
}
}
}
| Field | Type | Constraint | Notes |
|---|---|---|---|
kyc_kind |
enum | REQUIRED, STRICT | see §6 |
kyc_purpose |
enum | REQUIRED, see §6 | drives required document set |
regulator |
enum | REQUIRED, see §6 | RBI / IRDAI / SEBI / RTO / generic |
documents_to_verify |
array | REQUIRED, ≥1 | each entry is a doc to verify |
documents_to_verify[].doc_kind |
enum | REQUIRED, see §6 | |
documents_to_verify[].doc_number_masked |
string | REQUIRED | last-4 only (privacy) |
documents_to_verify[].verification_method |
enum | REQUIRED, see §6 | |
user_profile_provided |
object | REQUIRED, all fields REDACTED | partner receives full data via secure channel directly |
minimum_kyc_level_required |
enum | REQUIRED, STRICT min_kyc | full_kyc | video_kyc | in_person_kyc |
|
is_runtime_injected |
bool | REQUIRED | drives different success widget |
injected_from_intent |
string or null | REQUIRED | parent intent if injected |
context.trust_signals.previously_flagged |
bool | REQUIRED | prior KYC failure history |
Privacy note: TOMO never stores raw KYC data. The user_profile_provided block surfaces redacted previews; the actual document numbers + full identity data flow from user → partner directly via the partner's encrypted upload widget. TOMO's role is orchestration, not data custody.
Anti-fabrication preamble: no paid placement, no commission-based partner ordering, TOMO never holds raw KYC data.
3. PROVIDER TOOLS
Tool 1: initiate_kyc_session
PURPOSE: create a KYC verification session
INPUT: §2 request body
OUTPUT: { session_id, session_token, partner_kyc_url, expires_at, expected_minutes }
SLA: p95 < 1500ms
IDEMPOTENCY: REQUIRED on (user_session_id, kyc_purpose) — same purpose for same user returns same session_id within 24h
PRIVACY: partner returns a URL for user to complete KYC in partner's own surface
Tool 2: get_kyc_status
PURPOSE: poll session status
INPUT: { session_id, request_id }
OUTPUT: KycStatus (§5)
SLA: p95 < 600ms
RATE LIMIT: ≤ 1 every 5s
Tool 3: consume_kyc_result
PURPOSE: receive partner's verified KYC summary (after user completes)
INPUT: { session_id, request_id }
OUTPUT: KycVerificationResult (§5) — minimal data
SLA: p95 < 1000ms
RETURNS: verification_status booleans + reference IDs.
NO raw document data. NO scanned image URLs.
Tool 4: cancel_kyc_session
PURPOSE: user cancels mid-flow
INPUT: { session_id, reason, request_id }
OUTPUT: { status, partial_data_purged: bool, refund_to_purpose_intent: bool }
SLA: p95 < 1500ms
Tool 5: revoke_kyc
PURPOSE: user requests deletion of completed KYC (DPDP 2023 right-to-erasure)
INPUT: { session_id, reason, request_id, user_consent_token }
OUTPUT: { status, deletion_iso, downstream_partners_notified[] }
SLA: p95 < 5000ms (asynchronous delete OK)
All five REQUIRED. Tool 5 is non-negotiable for DPDP 2023 compliance.
4. RESPONSE SHAPE
initiate_kyc_session output
session_id: string, REQUIRED
session_token: string, REQUIRED # opaque, used in subsequent calls
partner_kyc_url: URL, REQUIRED # user-facing URL for KYC completion
url_expires_at: ISO_DATETIME, REQUIRED
expected_completion_minutes: int, REQUIRED # typical user time
session_expires_at: ISO_DATETIME, REQUIRED # session lifetime
method:
primary_method: STRICT ENUM, REQUIRED # see §6
fallback_methods: array<enum>, REQUIRED, may be empty
video_kyc_human_required: boolean, REQUIRED
video_kyc_avg_wait_minutes: int, REQUIRED # 0 if not video KYC
documents_required: array, REQUIRED, ≥1
- doc_kind: STRICT ENUM, REQUIRED
is_mandatory: boolean, REQUIRED
upload_methods: array<enum>, REQUIRED, ≥1 # see §6 (digilocker | upload | api)
sample_format: STRICT ENUM, REQUIRED # pdf | jpg | png | scanned
privacy:
data_minimization_enforced: boolean, REQUIRED # MUST be true
data_retention_days: int, REQUIRED # per regulator / DPDP
data_retention_purpose: string, REQUIRED # legal basis for retention
data_sharing_with_third_parties: boolean, REQUIRED # MUST be false unless regulator-mandated
user_consent_required: boolean, REQUIRED # MUST be true
user_can_revoke: boolean, REQUIRED # MUST be true (DPDP)
trust:
partner_iso_27001_certified: boolean, REQUIRED
partner_soc_2_type_2_audited: boolean, REQUIRED
partner_iso_27018_certified: boolean, REQUIRED # cloud privacy
partner_authorized_by: STRICT ENUM, REQUIRED # see §6
partner_license_number: string, REQUIRED
partner_license_valid_until: ISO_DATE, REQUIRED
digilocker_authorized: boolean, REQUIRED
uidai_authorized_user_agency: boolean, REQUIRED # for Aadhaar
uidai_aua_kua_kind: STRICT ENUM, REQUIRED # see §6
fees:
fee_inr: INR_INTEGER, REQUIRED # 0 if free for purpose intent
fee_charged_to: STRICT ENUM, REQUIRED # user | parent_intent_partner | tomo (NEVER tomo for v1)
refund_on_failure: boolean, REQUIRED
_provider:
name: string, REQUIRED
tomo_partner_id: string, REQUIRED
partner_tier: STRICT ENUM, REQUIRED
customer_support_phone: string, REQUIRED
customer_support_24x7: boolean, REQUIRED
in_app_chat_supported: boolean, REQUIRED
human_kyc_specialist_available: boolean, REQUIRED
KycStatus (returned by get_kyc_status)
session_id: string, REQUIRED
status: STRICT ENUM, REQUIRED # see §6
status_updated_iso: ISO_DATETIME, REQUIRED
status_history: array, REQUIRED, ≥1
- status: STRICT ENUM, REQUIRED
iso: ISO_DATETIME, REQUIRED
notes: string, REQUIRED # "" allowed
progress:
total_steps: int, REQUIRED, ≥1
completed_steps: int, REQUIRED, ≥0
current_step: STRICT ENUM, REQUIRED # see §6
estimated_remaining_minutes: int, REQUIRED
documents_received: array, REQUIRED, may be empty
- doc_kind: STRICT ENUM, REQUIRED
received_iso: ISO_DATETIME, REQUIRED
verification_status: STRICT ENUM, REQUIRED # pending | verified | failed | expired
verification_iso: ISO_DATETIME, REQUIRED # epoch sentinel if pending
failure_reason: STRICT ENUM, REQUIRED # see §6 ("none" if verified or pending)
identity_match:
name_match: STRICT ENUM, REQUIRED # exact | fuzzy_high | fuzzy_low | mismatch | pending
dob_match: STRICT ENUM, REQUIRED # match | mismatch | pending
address_match: STRICT ENUM, REQUIRED # match | partial_match | mismatch | pending
risk:
risk_score: int, REQUIRED, 0-100 # higher = more risky
risk_signals: array<enum>, REQUIRED, may be empty
watchlist_match: boolean, REQUIRED # PEP / sanctions
pep_status: boolean, REQUIRED # politically exposed person
support_phone: string, REQUIRED
support_email: string, REQUIRED
KycVerificationResult (returned by consume_kyc_result)
session_id: string, REQUIRED
result_id: string, REQUIRED # opaque KYC reference
verified_at_iso: ISO_DATETIME, REQUIRED
verification_kind: STRICT ENUM, REQUIRED # min_kyc | full_kyc | video_kyc | in_person_kyc
verification_validity_until_iso: ISO_DATE, REQUIRED # KYC expires; user re-verifies after
flags:
identity_verified: boolean, REQUIRED
address_verified: boolean, REQUIRED
pan_aadhaar_linked: boolean, REQUIRED
watchlist_clear: boolean, REQUIRED
pep_status: boolean, REQUIRED
age_at_least_18: boolean, REQUIRED
india_resident: boolean, REQUIRED
high_value_eligibility: boolean, REQUIRED # for transactions > ₹50L
# NO RAW DOCUMENT DATA RETURNED. NO SCANNED IMAGES.
# Partner's downstream products consume by referencing result_id with user consent.
audit_trail:
consent_iso: ISO_DATETIME, REQUIRED
consent_text_displayed: string, REQUIRED # exact text user agreed to
data_collected: array<STRICT ENUM>, REQUIRED # which doc kinds were collected
data_minimized: boolean, REQUIRED # MUST be true
retention_days: int, REQUIRED
retention_purpose: string, REQUIRED
_provider:
name: string, REQUIRED
tomo_partner_id: string, REQUIRED
Forbidden fields
paid_placement_score | sponsored_rank | promotion_priority |
ad_bid | hidden_kyc_fee | document_image_url | scanned_aadhaar_url |
raw_document_number_unmasked | partner_will_share_with_advertisers
Critical: TOMO ingest scans for any field containing raw document numbers, full names not masked, scanned image URLs. If detected, the entire response is rejected and the partner is flagged for DPDP 2023 violation.
5. CONTROLLED VOCABULARIES
kyc_kind
min_kyc | full_kyc | video_kyc | in_person_kyc
kyc_purpose
personal_loan | home_loan | credit_card | mutual_fund | demat_account |
trading_account | term_insurance_high_value | health_insurance |
self_drive_rental_first_time | high_value_marketplace_transaction |
employment_verification | tenant_verification | nbfc_credit_check |
generic_kyc
regulator
rbi | irdai | sebi | rto | generic
documents_to_verify[].doc_kind
aadhaar | pan | passport | driving_license | voter_id |
ration_card | bank_statement | utility_bill | property_doc |
employment_letter | itr_form_16 | gstin | shop_establishment_act
documents_to_verify[].verification_method
digilocker | uidai_otp | uidai_offline_xml | income_tax_api |
manual_upload_with_otp_verification | api_provider_verified |
video_kyc_face_match | in_person_visit
method.primary_method
digilocker_pull | aadhaar_otp_authentication | aadhaar_offline_xml |
video_kyc_with_human | video_kyc_automated | document_upload_plus_face_match |
in_person_branch_visit | dsa_field_visit
method.fallback_methods
Same enum as primary_method.
documents_required[].upload_methods
digilocker | direct_upload | api_pull | scan_via_app | physical_upload
documents_required[].sample_format
pdf | jpg | png | scanned_pdf | digilocker_xml | api_json
trust.partner_authorized_by
rbi_pa_psp | rbi_nbfc | irdai_corporate_agent | sebi_registered_intermediary |
mca_registered | uidai_aua_kua | digilocker_authorized_partner
trust.uidai_aua_kua_kind
not_applicable | aua_only | kua_only | aua_kua_both | sub_aua_kua
KycStatus.status
initiated | awaiting_user_action | docs_received | identity_verifying |
identity_verified | risk_assessment | watchlist_check |
verified | failed_doc | failed_identity_match | failed_watchlist |
failed_pep_blocked | timeout | cancelled_by_user | revoked_by_user
KycStatus.progress.current_step
not_started | digilocker_consent | aadhaar_otp_input | document_upload |
face_match | video_kyc_call | review_submitted | risk_assessment |
done
KycStatus.documents_received[].verification_status
pending | verified | failed | expired
KycStatus.documents_received[].failure_reason
none | doc_unreadable | doc_expired | doc_tampered | name_mismatch |
dob_mismatch | photo_mismatch | api_unavailable | uidai_otp_failed |
manual_review_required
KycStatus.identity_match.name_match
exact | fuzzy_high | fuzzy_low | mismatch | pending
KycStatus.identity_match.dob_match
match | mismatch | pending
KycStatus.identity_match.address_match
match | partial_match | mismatch | pending
KycStatus.risk.risk_signals
none | new_pan_within_30_days | aadhaar_recently_updated_address |
multiple_kyc_attempts_24h | mismatched_phone_number |
unusual_geographic_pattern | shared_device_with_other_kyc_user |
recently_blacklisted_in_partner_db | pep_match | sanctions_match
cancel_kyc_session.reason
user_changed_mind | wrong_documents | upload_failure | parent_intent_cancelled |
privacy_concern | technical_failure | other
KycVerificationResult.verification_kind
Same as kyc_kind enum.
audit_trail.data_collected[]
Same as documents_to_verify[].doc_kind enum.
_provider.partner_tier
tier1_path_a | tier1_path_b | tier1_path_c | tier2_path_d
6. TTBS DIMENSIONS
Per-domain weights (locked)
compliance: { time: 0.30, taste: 0.05, budget: 0.20, safety: 0.45 }
Safety weight dominates (highest in any TOMO intent — KYC is identity verification).
TIME
SIGNALS USED:
- method.expected_completion_minutes (lower = better) weight 0.40
- method.video_kyc_avg_wait_minutes weight 0.20
- method.primary_method == digilocker_pull (fastest) weight 0.20
- partner historical p95 completion time weight 0.20
USER BAND HANDLING:
- is_runtime_injected=true → time weight up to 0.45 (user blocked on parent intent)
- standalone → time weight steady
TASTE
SIGNALS USED:
- human_kyc_specialist_available weight 0.40
- in_app_chat_supported weight 0.30
- vernacular language support (per user_locale) weight 0.30
Taste weight is small (0.05) — user just wants KYC done.
BUDGET
SIGNALS USED:
- fees.fee_inr (lower = better) weight 0.50
- fees.fee_charged_to == user (penalty if true) weight 0.30
- refund_on_failure weight 0.20
HARD FILTERS:
- fees.fee_charged_to == tomo → drop (TOMO doesn't subsidize KYC)
SAFETY (the dominant axis)
SIGNALS USED:
- trust.partner_iso_27001_certified HARD FILTER
- trust.partner_authorized_by ∈ valid regulator enum HARD FILTER
- privacy.data_minimization_enforced=true HARD FILTER
- privacy.user_consent_required=true HARD FILTER
- privacy.user_can_revoke=true HARD FILTER
- trust.uidai_authorized_user_agency=true (Aadhaar use) HARD FILTER if Aadhaar in docs
- trust.partner_iso_27018_certified (cloud privacy) weight 0.20
- trust.partner_soc_2_type_2_audited weight 0.20
- privacy.data_sharing_with_third_parties=false HARD FILTER (must be false)
- partner_license_valid_until > now+30d HARD FILTER
- audit_trail.data_minimized=true HARD FILTER
- method.primary_method ∈ regulator-approved list HARD FILTER
- historical breach record (partner) weight 0.20 penalty
- partner_iso_27018_certified weight 0.20
User-context HARD FILTERS:
- kyc_purpose=personal_loan + minimum_kyc_level_required=full_kyc
→ method must support full_kyc → drop video_kyc_only partners
- kyc_purpose=high_value (>₹50L) → method must include video_kyc OR in_person
Hidden ranking factor
information_completeness_score weight 0.10.
historical_kyc_success_rate weight 0.20 — partners with >5% KYC failure rate get penalized.
7. COMPLETION CONTRACT
POST /api/v1/cpc/mcp_provider/{tomo_partner_id}
X-TOMO-Timestamp: <ms>
X-TOMO-Signature: sha256=<hex>
{
"intent": "compliance.verify_kyc",
"intent_version": "v1.0.0",
"external_id": "DIGIO-KYC-XYZ",
"amount_inr": 0,
"closed_at": "2026-05-10T08:42:00+05:30",
"request_id": "req_kyc_8f4k_...",
"status": "verified",
"currency": "INR",
"session_id": "DIGIO-KYC-XYZ",
"verified_at_iso": "2026-05-10T08:42:00+05:30",
"verification_kind": "full_kyc",
"verification_validity_until_iso": "2027-05-10",
"fees_charged_inr": 0,
"result_id": "kyc_result_abc123",
"documents_verified": ["aadhaar", "pan"],
"watchlist_clear": true,
"high_value_eligible": true,
"injected_from_intent": "finance.apply_personal_loan",
"injected_request_id": "req_pl_8f4k_...",
"notes": ""
}
Status enum: verified | failed_doc | failed_identity_match | failed_watchlist | failed_pep_blocked | cancelled_by_user | revoked_by_user
amount_inr is typically 0 because KYC fees are usually absorbed by the parent intent's partner (loan provider, insurance provider, etc.). When amount_inr > 0 (rare standalone KYC fee), TOMO commission applies normally.
Privacy: the completion POST includes ONLY metadata (verification status, validity date, document KINDS verified, eligibility flags). NEVER raw document data.
8. WIDGET
WIDGET TYPE: kyc_card
SOURCE: src/widgets/types.ts
TYPE NAME: KycCardPayload
RENDERED IN: components/widgets/KycCardWidget.tsx
Two render contexts:
- Standalone — full card with KYC kind, expected duration, partner trust badges, "Start KYC" CTA → opens partner_kyc_url in iframe or external.
- Runtime-injected — slim banner inside the parent intent's confirmation widget. Shows: "Verify your identity to proceed (estimated 4 min)" with inline "Verify now" button. Completion returns user to parent flow with KYC result attached.
9. CACHING POLICY
| Call | TTL | Rationale |
|---|---|---|
initiate_kyc_session |
0s | Always fresh |
get_kyc_status |
0s | Live progress |
consume_kyc_result |
0s | Always fresh |
cancel_kyc_session |
0s | |
revoke_kyc |
0s |
10. ERROR CODES
| Code | HTTP | Meaning | TOMO behavior |
|---|---|---|---|
INVALID_REQUEST |
400 | Malformed | surface |
INVALID_DOCUMENT_KIND |
400 | doc_kind not supported | surface |
UIDAI_DOWNTIME |
503 | UIDAI Aadhaar API down | suggest fallback method |
DIGILOCKER_DOWNTIME |
503 | DigiLocker down | suggest fallback method |
IDENTITY_MISMATCH |
422 | name/DOB don't match across docs | surface to user, retry |
WATCHLIST_BLOCKED |
403 | PEP/sanctions match | surface, terminate KYC |
DOC_EXPIRED |
422 | document past validity | surface, request new |
SESSION_TIMEOUT |
410 | user took too long | surface, restart |
INVALID_AUTH |
401 | partner credentials bad | partner re-auth |
RATE_LIMITED |
429 | partner-side throttle | back off |
INTERNAL_ERROR |
500 | partner-side failure | drop partner from pool |
MANUAL_REVIEW_REQUIRED |
202 | low-confidence verification | escalate to human |
11. SANDBOX → PRODUCTION CHECKLIST
[ ] All §2 inputs validated, request_id echoed
[ ] initiate_kyc_session returns valid partner_kyc_url within SLA
[ ] get_kyc_status returns progress updates ≤5s old
[ ] consume_kyc_result returns ONLY metadata (no raw doc data)
[ ] cancel_kyc_session purges partial data per privacy commitments
[ ] revoke_kyc properly purges + notifies downstream consumers (DPDP 2023)
[ ] All §4 required fields populated with REAL data
[ ] No forbidden fields anywhere (no raw doc URLs, no unmasked numbers)
[ ] CPC webhook arrives within 60s of verification completion
[ ] HMAC verification passes
[ ] ISO 27001 certificate uploaded + verified
[ ] ISO 27018 certificate uploaded if cloud-based
[ ] SOC 2 Type 2 audit report uploaded
[ ] RBI / IRDAI / SEBI authorization number valid + verifiable
[ ] UIDAI AUA/KUA authorization for Aadhaar verification (if applicable)
[ ] DigiLocker authorized partner attestation
[ ] Privacy policy URL live + DPDP 2023 compliant
[ ] Data retention period documented + matches regulator requirements
[ ] User consent UX reviewed for clarity
[ ] Failure recovery flow tested (UIDAI downtime, DigiLocker downtime)
[ ] customer_support 24x7 reachable + KYC specialist available
[ ] No commission-based partner ordering (1% audit cross-check)
[ ] Vernacular language support tested (en-IN, hi-IN minimum)
[ ] Field-test with TOMO ops doing real KYC
12. ANTI-FABRICATION RULES
RULE 1 — No paid placement signals
RULE 2 — No raw document data in any TOMO-bound response
TOMO ingest scans for unmasked Aadhaar / PAN / DL numbers, scanned image URLs,
full names not redacted. Detection = entire response rejected + DPDP violation flag.
RULE 3 — Data minimization mandatory
privacy.data_minimization_enforced MUST be true. Partners that collect more
than the kyc_purpose strictly requires = listing rejection.
RULE 4 — User consent must be auditable
audit_trail.consent_iso + consent_text_displayed must be exact. Partners
cannot retro-grant consent.
RULE 5 — User right-to-revoke must function
revoke_kyc must purge data within partner's documented retention SLA AND
notify downstream consumers. Failure = DPDP 2023 violation + suspension.
RULE 6 — No third-party data sharing without disclosure
privacy.data_sharing_with_third_parties MUST be false unless regulator-mandated
AND disclosed in user consent text.
RULE 7 — UIDAI authorization must be valid
trust.uidai_authorized_user_agency=true requires UIDAI license certificate
on demand within 24h. Failure = listing rejection.
RULE 8 — No commission-based partner ordering
Same KYC purpose on TOMO must rank by user-fit (TTBS, safety dominant),
not by partner commission.
RULE 9 — Customer support honest
Human KYC specialist available within stated SLA. Field-tested.
RULE 10 — No watchlist data caching beyond regulator window
PEP / sanctions data refreshed per regulator requirements. Stale data =
legal liability + breach.
RULE 11 — No forced upsells inside KYC flow
Partner cannot use KYC session to push other products. KYC is a utility,
not a sales surface.
RULE 12 — TOMO never holds raw KYC data
All raw document data flows user → partner directly. TOMO orchestrates
but does not custody.
VERSION HISTORY
v1.0.0 — 2026-05-10 — Initial spec (Block F.2)