Integrations • Financial Services API
Plaid Integration for Identity Verification and Bank Account Linking
Quick Links
Summary
- Problem: Automotive dealerships needed to verify customer identities (KYC/AML compliance) and check bank account balances before approving loans. Manual processes took 2-3 days, cost $20-30 per application, and resulted in 18% deal fallout from insufficient funds.
- Solution: Implemented comprehensive Plaid integration with dual implementation patterns (basic stateless + complete stateful), JSONB storage for flexible schemas, partner ID mapping for multi-tenant support, and environment-specific configuration.
- Impact: Identity verification: 2-3 days → 2-3 minutes (99% faster), KYC costs: $20-30 → $3-5 (75% reduction), Fund check accuracy: ~85% → 99.9%, Deal fallout: 18% → 4% (78% reduction)
- Key Decisions: Dual implementation pattern for flexibility, JSONB for schema changes without migrations, Partner ID mapping for multi-tenant scenarios, Environment-specific config to prevent sandbox/production mixups
Context
In automotive financing, dealerships face two critical verification challenges: (1) Identity Verification (KYC/AML Compliance) - Federal regulations require lenders to verify applicant identities per Bank Secrecy Act (BSA), Anti-Money Laundering (AML) checks, Customer Identification Program (CIP) requirements, and Patriot Act Section 326. Traditional methods (photocopied licenses, manual review) are slow, expensive ($15-30/application), and easily forged. (2) Financial Capability Verification - Before approving car loans, dealerships need to verify customers can afford monthly payments. Self-reported income is often inflated, pay stubs can be fabricated, manual bank statement review is slow and privacy-invasive, and 15-20% of approved loans fail at funding due to insufficient funds.
Symptoms / Failure Modes
- 2-3 day identity verification delays causing customer frustration
- $20-30 per application in KYC compliance costs
- 18% deal fallout from insufficient down payment funds
- 3-4 customer trips to dealership for paperwork verification
- No audit trail for regulatory compliance
- No automated KYC verification system
- No real-time bank balance checking capability
- No secure credential handling for bank authentication
- No multi-environment support (sandbox vs. production)
Goals, Requirements, Constraints
Goals
- Enable remote identity verification and bank balance checking
- Reduce KYC costs by 50%+
- Reduce deal fallout from insufficient funds by 60%+
- Create complete audit trail for compliance
- Support partner integrations with external dealer systems
Constraints
- Time: 3-month delivery with solo engineer
- Compliance: Bank Secrecy Act (BSA), Anti-Money Laundering (AML), GLBA for PII protection
- Cost: Minimize Plaid API calls ($3-5 per verification), control database storage growth
- Legacy: Integrate with existing contact/dealer management system, support partner ID systems
- Security: Secure credential handling with no credential storage permitted
Non-Goals
- Building custom KYC verification (use Plaid instead)
- Storing bank credentials (OAuth flow only)
- Supporting multiple verification vendors (Plaid only for MVP)
- Mobile app integration (web-first approach)
Acceptance Criteria
- Remote identity verification with government ID + selfie
- Real-time bank account balance checking
- OAuth-based bank authentication (no credential storage)
- Support both internal UUIDs and partner IDs
- Three-environment support (sandbox, development, production)
- Complete audit trail for all KYC and fund checks
- <3 minute identity verification time
- 99%+ fund check accuracy
Approach
The solution uses dual implementation patterns to support both stateless (basic) and stateful (complete) workflows, with JSONB storage for schema flexibility. This approach enables progressive adoption, simplifies testing, and provides clear separation of concerns where storage is optional rather than mandatory.
Key Design Decisions
- Decision: Dual Implementation Pattern (Basic + Complete endpoints)
Why: Different clients have different storage needs. Progressive adoption allows starting with basic endpoints and upgrading later. Simplifies testing as basic endpoints need no DB setup. Clear separation of concerns makes storage optional, not mandatory.
Alternatives: Single stateful implementation forces all clients to use database persistence. Single stateless implementation prevents recurring fund checks and audit trails. Dual pattern chosen for maximum flexibility across different use cases. - Decision: JSONB for Verification Data instead of normalized tables
Why: Plaid schema varies by verification method (document vs. database check). Plaid changed KYC result schema during development. Need complete data capture for auditing. Different countries/regions return different fields.
Alternatives: Fully normalized schema would require migrations for every Plaid schema change. NoSQL document store would lose ACID guarantees for financial data. JSONB hybrid chosen: core fields as columns, flexible data as JSONB. - Decision: Partner ID Mapping (support both internal UUIDs and partner IDs)
Why: Platform serves multiple dealership groups with their own ID systems. External systems should not need to translate IDs. Track data provenance to know which partner created each record.
Alternatives: Single ID system would force external integrations to maintain ID translation tables. Chosen dual-index approach allows native ID usage from any system. - Decision: Environment-Specific Configuration (sandbox, development, production)
Why: Plaid has different behaviors per environment. Different verification templates needed for sandbox vs. production. Prevent accidental production data in sandbox. Clear logging of current mode prevents costly mistakes.
Alternatives: Single configuration would risk production/sandbox data mixing. Manual environment switching would be error-prone. Automated environment detection with explicit logging chosen.
Implementation
Components / Modules
- Basic Endpoints (Stateless): Six endpoints for clients managing their own tokens: create-link-token, exchange-public-token, verify-account, check-funds, create-identity-verification, get-identity-verification
- Complete Endpoints (Stateful): Four endpoints with automatic database persistence: save-bank-verification, check-funds-by-contact, identity-verification-complete, identity-verification-complete/status
- Plaid Client Manager: Environment-specific client initialization with sandbox/development/production mode detection and appropriate credential loading
- Database Layer: Three tables: plaid_bank_verifications (bank connections), plaid_identity_verifications (KYC results), plaid_fund_checks (audit log)
Data & State
- plaid_bank_verifications: Stores active bank account connections with access_token, item_id, account_id. Status: active/invalid/expired. Retention: Active connections only, clean up expired.
- plaid_identity_verifications: Stores KYC verification results with verified_data JSONB (name, DOB, address, SSN) and kyc_details JSONB (check results). Status: active/success/failed/expired. Retention: Indefinite (regulatory requirement).
- plaid_fund_checks: Audit log for all fund checks with check_id, bank_verification_id, requested_amount, available_balance, has_sufficient_funds boolean. Retention: 7 years (compliance).
- JSONB schema: Core fields as columns for indexing/querying, flexible API response data in JSONB to handle schema changes without migrations
- Dual indexes on both contact_id (internal UUID) and dc_contact_id (partner ID) for efficient lookups from any system
Automation & Delivery
- GitHub Actions on push to main branch
- Automated test suite: go test ./...
- Binary build and deployment to Hetzner Cloud
- Health check validation post-deployment
- Unit tests for database layer
- Integration tests with Plaid sandbox environment
- Manual testing in development environment before production promotion
// Dual implementation pattern example
// Basic endpoint - client manages tokens
POST /plaid/check-funds
{
"access_token": "access-sandbox-xxx",
"account_id": "xxx",
"requested_amount": 5000
}
// Complete endpoint - system manages tokens
POST /plaid/check-funds-by-contact
{
"dc_contact_id": "DC123",
"requested_amount": 5000
}
// → Looks up stored access_token automatically
// Environment-specific initialization
func InitializePlaidClient() error {
plaidEnv := os.Getenv("PLAID_ENV")
var environment plaid.Environment
switch plaidEnv {
case "sandbox":
environment = plaid.Sandbox
case "development":
environment = plaid.Development
case "production":
environment = plaid.Production
default:
environment = plaid.Sandbox
}
log.Printf("INFO: Plaid client initialized in %s environment", plaidEnv)
} Notable Challenges
- Plaid Schema Changes: During development, Plaid changed their KYC result schema. JSONB storage absorbed the change without requiring database migrations - just updated Go structs. Lesson: JSONB flexibility is essential for third-party API integrations.
- Multi-Environment Complexity: Different environments require different template IDs and credentials. Solved with environment-specific configuration and explicit logging. Lesson: Explicit environment detection prevents costly production mistakes.
- Partner ID Mapping: External systems use their own ID formats. Solved by supporting both internal UUIDs and partner IDs with dual indexes. Lesson: Plan for multi-tenant scenarios from day one.
Security
- Unauthorized Access: Malicious access to KYC data or bank connections
- Credential Storage: Risk of storing bank login credentials (prohibited by Plaid ToS)
- Token Theft: Access tokens enable fund checks and account access
- API Key Exposure: Plaid credentials appearing in logs or version control
- Trust boundaries: Frontend ↔ Backend (JWT auth), Backend ↔ Database (localhost), Backend ↔ Plaid API (TLS + API keys), Customer ↔ Plaid (OAuth flow, credentials never touch our system)
Controls Implemented
- IAM / Least Privilege: Database user has INSERT, UPDATE, SELECT only (no DELETE). Plaid API keys in environment variables, never in code. Separate keys for sandbox/development/production.
- Secrets Management: Plaid credentials in .env (git-ignored). Production secrets in Hetzner Cloud secrets manager. Access tokens stored in database (encryption at rest planned).
- Audit Logging: All fund checks logged with timestamp, amount, result. All KYC verifications stored with complete results. 7-year retention for regulatory compliance.
- Network Security: Database localhost connection only. TLS for all external API calls. OAuth flow prevents credential exposure.
- Secure SDLC: gosec static analysis in CI/CD. Dependabot for dependency scanning. Manual review of all KYC data handling code.
Verification
- SAST: gosec automated in GitHub Actions for static code analysis
- Dependency Scanning: Weekly govulncheck runs for vulnerability detection
- Manual Review: All endpoints handling PII reviewed by security team
- Integration Testing: Plaid sandbox testing for OAuth flows and token handling
Operations
Observability
- Metrics: Plaid API response times (p50, p95, p99), Verification success rate (identity and bank), Access token validity rate (active vs. expired), Fund check pass/fail rate
- Critical Alert: Plaid API errors >5% in 5min window
- Warning Alert: Slow Plaid responses >3 seconds
- Info Alert: High expired token rate indicating re-linking needed
- Monitoring Stack: Sentry for error tracking + OpenTelemetry for distributed tracing
Incident Response
- Plaid API Downtime: Check Plaid status page, Notify dealers via email, Fall back to manual verification process
- Expired Access Tokens: User must re-link bank account, Update status to "expired" in database, Notify dealer to request new link from customer
- Verification Failures: Review kyc_details JSONB for specific failure reasons, Common causes: blurry ID photo, mismatch in provided data, Support can request manual review if needed
Cost Controls
- API Call Management: Cache link tokens with 30 min TTL to reduce redundant requests, Batch fund checks where possible, Track verification costs per dealer for cost allocation
- Vendor Decisions: Chose Plaid over Stripe Identity for superior bank integration capabilities, Saved $24K/year vs. custom KYC solution build
- Database Growth: Active connection cleanup for expired tokens, 7-year retention enforced via automated archival
Results
Outcomes
- Reliability: 99.9% fund check accuracy vs. ~85% self-reported data, eliminating majority of deal fallout from insufficient funds
- Security: Zero credential storage via OAuth flow, complete audit trail for regulatory compliance, 7-year retention meets BSA/AML requirements
- Cost: 75% reduction in KYC compliance costs ($20-30 → $3-5 per verification), $24K/year saved vs. custom solution
- Delivery: 99% faster identity verification (2-3 days → 2-3 minutes), reduced customer trips to dealership by 66% (3-4 visits → 1 visit)
| Metric | Before | After |
|---|---|---|
| Identity Verification Time | 2-3 days | 2-3 min |
| KYC Cost/Application | $20-30 | $3-5 |
| Fund Check Accuracy | ~85% | 99.9% |
| Deal Fallout Rate | 18% | 4% |
| Customer Trips Required | 3-4 visits | 1 visit |
Tradeoffs
- Dual implementation complexity vs. flexibility: Maintaining both basic and complete endpoints adds code complexity, but enables progressive adoption and different use cases. Monitoring usage of each endpoint type to potentially deprecate if unused.
- JSONB storage vs. normalized schema: Lose some referential integrity checks, but gain schema flexibility for volatile third-party APIs. Validating JSONB structure in application code and logging parsing errors.
- Access token plaintext storage: Currently storing access tokens in plaintext (encryption at rest planned for Q1 2024). Short-term risk accepted to meet delivery timeline, with encryption roadmapped.
- Polling vs. webhooks for verification status: Current implementation polls Plaid API for verification status. Webhook endpoint implementation planned when Plaid adds support (Q3 2024).
Next Steps
- Q1 2024: Access Token Encryption - Implement AES-256 encryption for stored access tokens, add key rotation mechanism (estimated 16 hours)
- Q2 2024: Webhook Support - Add webhook endpoint for verification status updates, implement real-time UI updates via WebSocket (estimated 24 hours)
- Q3 2024: Enhanced Status Tracking - Add status_detail field for failure reasons, track status update provenance, provide better error messages to dealers
- Risk Mitigation: Address access token security with encryption at rest, automate re-linking notification flow for expired tokens, implement request queuing for Plaid API rate limit protection