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: no field may contain raw document numbers, unmasked full names or scanned image URLs. Our team checks for these during review; a listing that carries them is rejected.
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 (checked by our team)
[ ] 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
No unmasked Aadhaar / PAN / DL numbers, scanned image URLs or unredacted full names.
Our team checks for these during review; a listing that carries them is rejected.
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)