KYC Provider Integrations¶
This document describes the four KYC provider integrations that
land in v0.9. Each provider has a sandbox endpoint that can be
hit without credentials; production deployments set the partner
URL and load the partner credentials from the configured
SecretsManager.
Configuration¶
{
"kyc_provider_config": {
"pan_client_id": "...",
"pan_client_secret": "...",
"pan_api_base_url": "https://api.karza.in",
"aadhaar_kua_id": "...",
"aadhaar_kua_license_key": "...",
"aadhaar_api_base_url": "https://www.uidai.gov.in",
"cibil_partner_id": "...",
"cibil_partner_key": "...",
"cibil_api_base_url": "https://api.cibil.com",
"ckyc_search_provider_id": "...",
"ckyc_search_provider_key": "...",
"ckyc_api_base_url": "https://search.ckycindia.in",
"timeout_seconds": 30
}
}
Secret-shaped fields (*_secret, *_key, *_id, *_token) are
never persisted in the config file — they are read from the
configured SecretsManager (Vault, AWS SM, or env) at startup.
Configuration.to_dict() redacts them, so config.save() cannot
leak them.
The following env vars opt the clients into their production endpoints (sandbox by default):
UNDERWRITE_PAN_PRODUCTION=true— point PAN at the live Karza/Signzy endpointUNDERWRITE_AADHAAR_PRODUCTION=true— point Aadhaar at the UIDAI production KUAUNDERWRITE_CIBIL_PRODUCTION=true— point CIBIL at the partner production APIUNDERWRITE_CKYC_PRODUCTION=true— point CKYC at the CERSAI production search endpoint
Common surface¶
Every provider client extends Provider (in
underwrite.services.providers) and exposes the same shape:
is_configured()— returnsTrueonly when the client has the credentials it needs to call the real upstream APIverify(**kwargs)— runs a verification and returns aProviderResultcarrying aVerdictand the provider's structured response. The identifier (PAN, Aadhaar number, consumer_id, CKYC id) is bound at__init__time, not passed toverify().
Construction uses provider-specific kwarg names so each domain identifier reads naturally:
from underwrite.services.providers import (
Aadhar,
Pan,
Cibil,
Ckyc,
Verdict,
ProviderResult,
ProvidersConfig,
)
# Identifier is the first positional arg of every client.
client = Pan(pan="ABCDE1234F", client_id="...", client_secret="...")
result = client.verify(name="John", consent="Y")
Verdict is one of:
Verdict.VERIFIED— provider confirms the recordVerdict.NOT_FOUND— no record at the providerVerdict.MISMATCH— input was malformed or didn't matchVerdict.AMBIGUOUS— provider returned a borderline resultVerdict.REJECTED— DPDPA consent missing or record invalidVerdict.ERROR— transport or upstream failure
Clients never raise on transport or upstream failure; they
return Verdict.ERROR with a descriptive message in the
error field and log the underlying exception with
logger.exception(...).
PAN (ITD / NSDL)¶
Endpoint: POST {api_base_url}/v2/pan/verify
Wire request:
Wire response:
{
"request_id": "...",
"status": "VALID",
"pan_status": "ACTIVE",
"pan_type": "Individual",
"first_name": "John",
"last_name": "Doe",
"aadhaar_seeding_status": "Y"
}
The request body is HMAC-SHA256 signed with client_secret;
the signature is sent in the x-signature header. The Karza
sandbox is the default; Signzy uses the same wire shape with a
different api_base_url.
Aadhaar eKYC (UIDAI KUA)¶
Endpoint: POST {api_base_url}/eKYC/v3/auth/
Wire request:
Wire response (after the KUA SDK decrypts the auth XML):
{
"reference_id": "...",
"status": "Y",
"name": "John Doe",
"dob": "1990-01-01",
"gender": "M",
"address": {...},
"photo": "<base64>"
}
The base client hits the UIDAI staging endpoint as a shape
reference. Production deployments override
send_kyc_request to plug in the KUA SDK
(pyuid / okhota / proprietary); the override should call
the KUA's PKI-encrypted transport, decrypt the auth XML, and
return the same dict shape.
CIBIL consumer bureau pull¶
Endpoint: POST {api_base_url}/v2/cibil/score
Wire request:
{
"consumer_id": "...",
"name": "John Doe",
"dob": "1990-01-01",
"pan": "ABCDE1234F",
"address": {...},
"consent": "Y"
}
Wire response:
{
"request_id": "...",
"score": 750,
"score_band": "Excellent",
"tradelines": 5,
"enquiries_last_30_days": 1,
"defaults": []
}
The bureau score in details["score"] is an integer in the
300-900 range; values outside that range produce
Verdict.AMBIGUOUS rather than Verdict.VERIFIED.
CKYC registry search (CERSAI)¶
Endpoint: POST {api_base_url}/v1/ckyc/search
Wire request:
Wire response:
{
"request_id": "...",
"ckyc_number": "110000001234",
"name": "John Doe",
"dob": "1990-01-01",
"pan": "ABCDE1234F",
"aadhaar_last4": "1234",
"address": {...},
"image_present": true,
"kyc_status": "VERIFIED"
}
The identifier_type argument selects the search mode:
"ckyc_number", "pan", or "aadhaar". Any other value
returns Verdict.ERROR.
Service integration¶
The compliance and credit_bureau services consume the
configured providers via the runtime-injected
kyc_provider_config (ProvidersConfig). Without a
provider config, both fall back to format-only validation;
with a provider config, the real upstream call gates the
service's KYC verdict. Handlers construct a client per call
via ProvidersConfig.resolve_pan(pan, secrets).
# Runtime auto-wiring — no application code required:
Runtime(
config, # has kyc_provider_config populated from Configuration
).register("compliance")
Sandbox vs production¶
The four providers default to the public sandbox endpoints
because the sandbox is what the partner gives you for free
during integration. Production deployments set the matching
UNDERWRITE_*_PRODUCTION=true env var and set the
corresponding *_api_base_url to the partner's live endpoint.
The client itself does not switch endpoints automatically; the URL is a runtime configuration value, and the production gate is a separate flag. This is intentional: it lets a staging deployment use the production partner URL against a partner sandbox tenant, which is a different configuration from "production with the partner's live API".