DOMAIN:EU_REGULATION:KYC_IMPLEMENTATION¶
OWNER: eric ALSO_USED_BY: julian, aimee, hugo, ALL dev agents building regulated features UPDATED: 2026-03-26 SCOPE: Technical implementation guide for KYC features — providers, integrations, schemas, flows
PURPOSE¶
This page provides technical guidance for implementing KYC in software.
Covers identity verification providers, integration patterns, database design, and audit trail requirements.
For regulatory context see kyc-aml.md. For process detail see kyc-processes.md.
EU-BASED IDENTITY VERIFICATION PROVIDERS¶
PROVIDER COMPARISON¶
| Provider | HQ | Strengths | Best For | Pricing | Compliance |
|---|---|---|---|---|---|
| Signicat | Norway | Nordic eID schemes, eSignatures, BankID | Nordic/EU regulated sectors | Enterprise | eIDAS, ETSI |
| Veriff | Estonia | 6-sec avg, 98% automation, 12000+ doc types | Fintech, crypto, marketplaces | Mid-high | ISO 30107-3, iBeta L2 |
| iDenfy | Lithuania | Pay-per-success, human-in-loop, 3D liveness | SMBs, startups | Flexible | ISO 30107-3 |
| IDnow | Germany | VideoIdent (human), AutoIdent (AI), BaFin-approved | German regulated markets | Enterprise | BaFin, eIDAS |
| Sumsub | UK | End-to-end KYC/KYB/AML, fraud prevention | Fintech, crypto, platforms | Mid | SOC 2, ISO 27001 |
| Onfido | UK (Entrust) | AI-driven doc + biometric, 1100+ orgs | Broad EU/UK market | Mid-high | ISO 27001, SOC 2 |
POST-BREXIT ASSESSMENT (UK PROVIDERS)¶
STATUS: EU adequacy decision renewed Dec 2025, valid until Dec 2031. IMPLICATION: data transfers to UK-based providers (Onfido, Sumsub) remain lawful under GDPR. RISK: UK Data (Use and Access) Act 2025 may widen GDPR divergence — monitor EDPB assessments. MITIGATION: ensure UK provider uses EU data centres and Standard Contractual Clauses (SCCs). RECOMMENDATION: prefer EU-headquartered providers (Veriff, iDenfy, IDnow, Signicat) for new integrations. REASON: removes dependency on adequacy decision renewal and reduces regulatory uncertainty.
PROVIDER SELECTION CRITERIA¶
MUST_HAVE: - ISO 30107-3 or iBeta Level 2 certification for liveness detection - EU data centres (or contractual commitment to EU data residency) - GDPR DPA (Data Processing Agreement) with clear processor role - webhook support for async verification results - sandbox/test environment - document coverage for EU/EEA ID documents (minimum)
SHOULD_HAVE: - NFC chip reading capability - eIDAS 2.0 / EUDI Wallet readiness (for future-proofing) - KYB (Know Your Business) features for legal entity verification - ongoing monitoring/screening (PEP, sanctions) - multi-language support (NL, EN, DE, FR at minimum for EU projects) - SOC 2 Type II certification
NICE_TO_HAVE: - white-label/embedded SDK - native mobile SDK (iOS Swift, Android Kotlin) - address verification - proof of address document verification - fraud signal sharing
NL-SPECIFIC VERIFICATION TOOLS¶
iDIN (Bank-Based Identification)¶
WHAT: digital identification via Dutch banks (ABN AMRO, ING, Rabobank, etc.) HOW: customer authenticates with their bank — bank shares verified identity attributes ATTRIBUTES: name, date of birth, gender, address (bank-verified) ASSURANCE: HIGH — bank has already performed KYC on the individual USE_CASE: supplement or replace document verification for Dutch residents LIMITATION: only works for customers with a Dutch bank account LIMITATION: does not provide photo or biometric data INTEGRATION: via Currence (iDIN scheme operator) or through PSP (Mollie, Adyen)
WHEN_TO_USE: - standard CDD for Dutch natural persons - fast onboarding where bank-verified identity is sufficient - combine with document photo for biometric match if needed
DigiD¶
WHAT: Dutch government digital identity for citizens/residents ASSURANCE: SUBSTANTIAL to HIGH (depending on authentication level) LEVELS: - DigiD Basis: username + password (low assurance — not sufficient for KYC) - DigiD Midden: + SMS verification (substantial) - DigiD Hoog: + ID document scan or DigiD app with NFC (high) USE_CASE: government services, healthcare — NOT typically available for private-sector KYC LIMITATION: only available for interactions with government-connected services NOTE: eIDAS 2.0 EUDI Wallet will eventually supersede DigiD for cross-border use
KvK API (Handelsregister API)¶
ENDPOINT: api.kvk.nl AUTH: API key (register at developers.kvk.nl) RETURNS: legal name, KvK number, legal form, registration date, SBI codes, addresses DOES_NOT_RETURN: UBO data, financial data RATE_LIMIT: depends on subscription tier COST: per-query pricing (check current kvk.nl developer portal)
USE_IN_KYC: - automate business client verification at onboarding - validate KvK number provided by client - retrieve and store authorised signatory information - periodic re-verification via scheduled API calls
UBO API (coming Q2 2026)¶
STATUS: KvK announced UBO API availability from Q2 2026 ACCESS: requires eHerkenning authentication RESTRICTION: access level depends on organisation type and legal basis FOR_GE: GE is not a Wwft obliged entity — access level to be confirmed ALTERNATIVE: request UBO declaration directly from the client
INTEGRATION PATTERNS¶
REDIRECT FLOW¶
HOW: user is redirected from your application to the provider's verification page. AFTER: provider redirects back with a verification session ID. RESULT: your backend polls or receives webhook with verification outcome.
PROS: - simplest integration (just a URL redirect) - provider handles all UI, camera access, document capture - provider handles browser/device compatibility
CONS: - user leaves your application (brand continuity broken) - less control over UX - mobile: may open browser from app (context switch)
BEST_FOR: MVPs, low-volume applications, quick time-to-market.
EMBEDDED SDK¶
HOW: provider's SDK is embedded in your frontend (web SDK or mobile SDK).
WEB_SDK: - JavaScript/TypeScript SDK loaded in your page - renders verification flow in an iframe or overlay - communicates results via JavaScript callbacks or postMessage
MOBILE_SDK: - native iOS (Swift) or Android (Kotlin) SDK - integrated into your app's navigation flow - provides camera access, NFC reading, liveness detection natively
PROS: - user stays in your application - brand-consistent UX - better mobile experience (native camera, NFC) - more control over flow and error handling
CONS: - more integration effort - SDK version management and updates - larger app bundle size (mobile)
BEST_FOR: production applications, mobile-first products, UX-sensitive projects.
API-ONLY¶
HOW: your application captures images/data and sends to provider API. YOU_HANDLE: camera capture, document scanning, selfie capture PROVIDER_HANDLES: document analysis, liveness analysis, matching, decision
PROS: - full control over UX - no third-party UI code - works for backend-to-backend integrations
CONS: - most complex integration - you own camera capture quality (significant source of failures) - you must handle image quality validation before submission - may require building liveness detection UI
BEST_FOR: highly customised flows, enterprise integrations, server-to-server processing.
DOCUMENT PROCESSING¶
OCR PIPELINE¶
CAPTURE → QUALITY_CHECK → CLASSIFY → EXTRACT → VALIDATE → STORE
- CAPTURE: user photographs or uploads document image
- QUALITY_CHECK: verify image resolution, blur, glare, cropping
- CLASSIFY: determine document type (passport, ID card, driving licence, etc.)
- EXTRACT: OCR text from visual zone; parse MRZ if present
- VALIDATE: cross-check MRZ against visual zone data; verify check digits
- STORE: encrypted, access-controlled storage of document image and extracted data
QUALITY_GUIDANCE_FOR_USERS: - flat surface, good lighting, no shadows - all four corners visible - no fingers covering text or photo - both sides for ID cards
MRZ READING¶
FORMAT: ICAO Doc 9303 PASSPORT: 2-line MRZ (44 chars each) ID_CARD: 3-line MRZ (30 chars each)
FIELDS_EXTRACTED: - document type - issuing country (3-letter code) - surname, given names - document number + check digit - nationality - date of birth + check digit - sex - expiry date + check digit - composite check digit
VALIDATION: - compute check digits and compare against MRZ values - any mismatch = potential tampering or OCR error - cross-reference extracted name/DOB against visual zone
NFC CHIP VERIFICATION¶
PROTOCOL: ISO 14443 (contactless communication) DATA_GROUPS: DG1 (MRZ data), DG2 (facial image), DG3 (fingerprints, if stored)
PASSIVE_AUTHENTICATION: - read Security Object (SOD) from chip - verify digital signature chain: chip → document signer → country signing CA - proves data was written by legitimate issuing authority - does NOT prove chip has not been cloned
ACTIVE_AUTHENTICATION: - chip signs a random challenge with its private key - proves the physical chip is original (not a clone) - not all documents support this
CHIP_ACCESS_CONTROL: - BAC (Basic Access Control): requires MRZ data to unlock chip (legacy) - PACE (Password Authenticated Connection Establishment): more secure, newer documents - reading app must implement both BAC and PACE for broad compatibility
MOBILE_IMPLEMENTATION: - iOS: Core NFC framework (iPhone 7+) - Android: android.nfc package - user holds document against phone's NFC antenna - read time: 2-5 seconds typically
LIVENESS DETECTION IMPLEMENTATION¶
ACTIVE LIVENESS¶
FLOW: 1. prompt user to position face in frame 2. request specific action: turn head left, turn head right, blink, smile 3. capture video frames during action 4. analyse motion, depth, texture 5. return liveness score + confidence
ACCESSIBILITY: - must provide alternative for users with mobility restrictions - consider: passive liveness fallback + manual review
PASSIVE LIVENESS¶
FLOW: 1. prompt user to take selfie or short video (no specific actions) 2. AI analyses image/video for: skin texture, depth cues, light reflection, micro-movements 3. detect: printed photos, screen replay, 3D masks, deepfake injection 4. return liveness score + confidence
INJECTION_ATTACK_DETECTION: - detect virtual cameras, screen recording, video injection at SDK level - verify image metadata (EXIF, device info) - provider SDK should validate camera stream integrity
DATABASE SCHEMA (DRIZZLE PATTERNS)¶
KYC RECORDS TABLE¶
export const kycRecords = pgTable('kyc_records', {
id: uuid('id').defaultRandom().primaryKey(),
clientId: uuid('client_id').notNull().references(() => clients.id),
verificationType: text('verification_type').notNull(), // 'individual' | 'business'
status: text('status').notNull().default('pending'),
riskLevel: text('risk_level'), // 'low' | 'medium' | 'high'
dueDiligenceLevel: text('due_diligence_level'), // 'sdd' | 'cdd' | 'edd'
providerSessionId: text('provider_session_id'), // external provider reference
providerName: text('provider_name'), // 'veriff' | 'idenfy' | 'idnow' | etc.
verifiedAt: timestamp('verified_at'),
expiresAt: timestamp('expires_at'), // next periodic review due date
reviewedBy: text('reviewed_by'), // agent or person who reviewed
rejectionReason: text('rejection_reason'),
metadata: jsonb('metadata'), // provider-specific data
createdAt: timestamp('created_at').defaultNow().notNull(),
updatedAt: timestamp('updated_at').defaultNow().notNull(),
});
KYC CHECKS TABLE¶
export const kycChecks = pgTable('kyc_checks', {
id: uuid('id').defaultRandom().primaryKey(),
kycRecordId: uuid('kyc_record_id').notNull().references(() => kycRecords.id),
checkType: text('check_type').notNull(),
// 'document_verification' | 'liveness' | 'face_match' | 'pep_screening' |
// 'sanctions_screening' | 'ubo_verification' | 'kvk_extract' | 'address_verification'
status: text('status').notNull().default('pending'),
// 'pending' | 'passed' | 'failed' | 'needs_review' | 'expired'
score: real('score'), // provider confidence score (0-1)
result: jsonb('result'), // provider-specific result payload
performedAt: timestamp('performed_at'),
performedBy: text('performed_by'), // 'automated' | agent name
notes: text('notes'), // reviewer notes
createdAt: timestamp('created_at').defaultNow().notNull(),
});
KYC DOCUMENTS TABLE¶
export const kycDocuments = pgTable('kyc_documents', {
id: uuid('id').defaultRandom().primaryKey(),
kycRecordId: uuid('kyc_record_id').notNull().references(() => kycRecords.id),
documentType: text('document_type').notNull(),
// 'passport' | 'id_card' | 'driving_licence' | 'residence_permit' |
// 'kvk_extract' | 'ubo_declaration' | 'proof_of_address' | 'shareholder_register'
storagePath: text('storage_path').notNull(), // encrypted storage location
extractedData: jsonb('extracted_data'), // OCR/MRZ extracted fields
expiryDate: date('expiry_date'), // document expiry
issuingCountry: text('issuing_country'), // ISO 3166-1 alpha-2
retainUntil: date('retain_until').notNull(), // 5 years after relationship end
deletedAt: timestamp('deleted_at'), // soft delete after retention period
createdAt: timestamp('created_at').defaultNow().notNull(),
});
KYC AUDIT LOG TABLE¶
export const kycAuditLog = pgTable('kyc_audit_log', {
id: uuid('id').defaultRandom().primaryKey(),
kycRecordId: uuid('kyc_record_id').notNull().references(() => kycRecords.id),
action: text('action').notNull(),
// 'created' | 'check_performed' | 'status_changed' | 'risk_updated' |
// 'reviewed' | 'approved' | 'rejected' | 'escalated' | 'document_uploaded' |
// 'document_deleted' | 'periodic_review' | 'sar_filed'
previousValue: jsonb('previous_value'),
newValue: jsonb('new_value'),
performedBy: text('performed_by').notNull(), // agent name or 'system'
reason: text('reason'),
ipAddress: text('ip_address'),
createdAt: timestamp('created_at').defaultNow().notNull(),
});
INDEX RECOMMENDATIONS¶
CREATE INDEX idx_kyc_records_client ON kyc_records(client_id);
CREATE INDEX idx_kyc_records_status ON kyc_records(status);
CREATE INDEX idx_kyc_records_expires ON kyc_records(expires_at) WHERE status = 'approved';
CREATE INDEX idx_kyc_checks_record ON kyc_checks(kyc_record_id);
CREATE INDEX idx_kyc_checks_type_status ON kyc_checks(check_type, status);
CREATE INDEX idx_kyc_documents_record ON kyc_documents(kyc_record_id);
CREATE INDEX idx_kyc_documents_retain ON kyc_documents(retain_until) WHERE deleted_at IS NULL;
CREATE INDEX idx_kyc_audit_record ON kyc_audit_log(kyc_record_id);
CREATE INDEX idx_kyc_audit_action ON kyc_audit_log(action, created_at);
STATUS MACHINE¶
KYC RECORD STATES¶
┌──────────────┐
│ pending │
└──────┬───────┘
│ checks started
┌──────▼───────┐
┌─────┤ in_review ├─────┐
│ └──────┬───────┘ │
│ │ │
┌────────▼───┐ ┌─────▼─────┐ ┌───▼──────────┐
│ approved │ │ rejected │ │ more_info │
└────────┬───┘ └───────────┘ │ _needed │
│ └───────┬──────┘
│ │ info provided
│ ┌───────▼──────┐
│ │ in_review │
│ └──────────────┘
│
┌────────▼───────┐
│ periodic_review │ (triggered by expiry date)
└────────┬───────┘
│
┌────────▼───┐
│ in_review │ (back to review cycle)
└────────────┘
TRANSITIONS: - pending → in_review: first verification check initiated - in_review → approved: all required checks passed, reviewer approves - in_review → rejected: check failed and cannot be remediated - in_review → more_info_needed: additional documentation required from customer - more_info_needed → in_review: customer provides requested information - approved → periodic_review: scheduled review date reached - periodic_review → in_review: review process started - any state → rejected: if sanctions match confirmed or relationship must be exited
CONSTRAINTS: - only "approved" status permits active business relationship - "pending" and "in_review" may proceed with limited activity if risk is low - "rejected" is final unless overridden with documented justification - every transition MUST be logged in kyc_audit_log
WEBHOOK PATTERNS¶
RECEIVING VERIFICATION RESULTS¶
FLOW: 1. your backend creates a verification session via provider API 2. provider returns session ID and redirect URL (or SDK token) 3. user completes verification on provider side 4. provider sends webhook to your registered endpoint 5. your backend processes result, updates KYC record status
WEBHOOK_SECURITY: - verify webhook signature (HMAC or RSA, provider-specific) - validate source IP against provider's published IP ranges (if available) - use HTTPS endpoint only - implement idempotency: process each webhook event at most once - use webhook ID or event ID as deduplication key
WEBHOOK_PAYLOAD (typical):
{
"event": "verification.completed",
"session_id": "abc-123",
"status": "approved",
"checks": {
"document": { "status": "passed", "confidence": 0.97 },
"liveness": { "status": "passed", "confidence": 0.99 },
"face_match": { "status": "passed", "confidence": 0.94 }
},
"extracted_data": {
"first_name": "Jan",
"last_name": "de Vries",
"date_of_birth": "1985-03-15",
"document_number": "ABC123456",
"nationality": "NLD"
},
"timestamp": "2026-03-26T14:30:00Z"
}
RETRY_HANDLING: - provider will retry failed webhook deliveries (typically 3-5 times) - implement 200 OK response immediately, process asynchronously - if webhook missed: poll provider API as fallback
FALLBACK POLLING¶
WHEN: webhook not received within expected timeframe (provider-dependent, typically 5-15 min). HOW: poll provider's session status API with exponential backoff. MAX_POLL: 24 hours, then flag for manual review.
AUDIT TRAIL REQUIREMENTS¶
WHAT MUST BE LOGGED¶
EVERY_CHECK: - check type, timestamp, result, confidence score - provider name and session ID - raw provider response (stored encrypted)
EVERY_DECISION: - who made the decision (agent name or "automated") - what decision was made (approve, reject, request more info, escalate) - reasoning for the decision - timestamp
EVERY_DOCUMENT: - document type, issuing country, expiry date - upload timestamp - who uploaded (customer or agent) - storage location (encrypted reference) - retention period and scheduled deletion date
EVERY_SCREENING: - lists screened against - matches found (including false positives) - investigation notes for each match - clearance reasoning for false positives
IMMUTABILITY¶
RULE: audit log entries must NEVER be modified or deleted. IMPLEMENTATION: append-only table, no UPDATE or DELETE permissions on kyc_audit_log. BACKUP: audit log included in database backup rotation. RETENTION: audit logs retained for same period as KYC records (5 years + buffer).
REGULATORY AUDIT READINESS¶
MUST_PRODUCE_ON_REQUEST: - complete KYC file for any customer (all checks, decisions, documents) - timeline of all actions taken - evidence of periodic reviews performed - evidence of sanctions/PEP screening and results - risk assessment and reasoning
FORMAT: must be retrievable in machine-readable format (AMLR requirement from 2027).
RE-VERIFICATION TRIGGERS¶
AUTOMATIC TRIGGERS¶
PERIODIC_REVIEW_DUE: - based on risk level: 12/24/36 month cycles - system generates review task when expires_at is reached - assign to Eric (for GE clients) or appropriate compliance agent
DOCUMENT_EXPIRY: - passport/ID card approaching expiry - trigger re-verification 3 months before document expiry
SANCTIONS_LIST_UPDATE: - when monitored sanctions lists are updated - re-screen affected customer segments
MANUAL TRIGGERS¶
MATERIAL_CHANGE: - change of ownership (UBO change) - change of business activity - change of registered address - change of directors/signatories
RISK_ESCALATION: - adverse media report - suspicious transaction detected - intelligence from law enforcement - client associated with investigated entity
REGULATORY_CHANGE: - new sanctions regime applicable - client's sector becomes regulated - jurisdiction risk level changes
eIDAS 2.0 / EUDI WALLET PREPARATION¶
TIMELINE: - Dec 2026: all EU Member States must provide EUDI Wallets to citizens - Nov 2027: mandatory acceptance by banks, PSPs, large online platforms - 2030: EU target of 80% citizen adoption
WHAT_TO_BUILD_NOW: - design KYC flows to accept alternative identity credentials (not just document scan) - abstract identity verification behind provider interface - plan for wallet-based selective disclosure (verify attributes without full identity) - monitor EUDI Wallet technical specifications (ongoing through 2026)
WHAT_NOT_TO_BUILD_YET: - no production-ready EUDI Wallets exist as of Mar 2026 - implementing specifications and APIs are still in draft - wait for stable technical standards before implementation - plan to integrate once Member State wallet apps are available
ARCHITECTURE_PRINCIPLE: - design verification interface to be provider-agnostic - support multiple verification methods per flow - wallet verification will be one method alongside document + liveness
READ_ALSO: kyc-aml.md, kyc-processes.md, kyc-data-retention.md, kyc-for-platforms.md