Loan underwriting with bank statements
How to use Bank Statement Platform's parsed ledger + Document Intelligence's NPWP/slip-gaji + AML screening to produce a decision-grade input to your credit model.
The shape
Loan applicant submits:
- 3-6 months of bank statements (PDF, possibly password-protected)
- NPWP card (image or PDF)
- Recent pay slip (slip gaji)
- Optional: collateral documents
▼
1. POST /api/analyses/upload (Bank Statement) × N statements
With auto_create_group=true so they consolidate
▼ (async) wait for "consolidation.completed" webhook
2. GET /api/consolidations/{id}/consolidated
← averageMonthlyInflow, affordabilityScore, anomalies
▼
3. POST /api/documents/upload (Document Intelligence) × 1 per doc
type=npwp / slip_gaji
▼ (async) wait for "document.extracted" webhooks
4. POST /api/screenings (AML)
lane=onboarding, NIK from KTP capture or NPWP
▼
5. Compose underwriting signal:
- Affordability (Bank Statement)
- Income (NPWP-declared + slip gaji + statement inflow)
- Authenticity flags (Bank Statement anomalies, NPWP confidence)
- Sanctions/PEP status (AML)
▼
6. Feed to your credit modelSignals worth surfacing
From Bank Statement's consolidated response:
| Signal | Underwriting use |
|---|---|
averageMonthlyInflow | Repayment-capacity baseline |
affordabilityScore | Pre-computed normalized score |
monthlyBuckets variance | Income stability — low variance = predictable repayment |
categoryBreakdown.salary vs total inflow | Income concentration — salaried vs gig workers |
categoryBreakdown.loan_repayment total | Existing debt service ratio |
anomalyCount + anomalies[] | Possible doctored statements — investigate before approving |
authenticityScore < 80 | Hard stop or manual review |
From Document Intelligence (NPWP):
| Signal | Underwriting use |
|---|---|
npwp_number | Verify against statement's account holder |
registered_at | Tax-history length |
From slip gaji:
| Signal | Underwriting use |
|---|---|
gross_salary, net_pay | Cross-check against statement salary credits |
deductions | PPh21 deducted at source → income legitimacy |
Decision matrix (example)
| Signal combination | Action |
|---|---|
| Affordability ≥ 70 + authenticity ≥ 80 + AML no_match | Auto-approve up to tier-1 limit |
| Affordability 50-70 + authenticity ≥ 80 + AML no_match | Manual underwriting |
| Affordability ≥ 50 + anomaliesCount > 0 | Anomaly review |
| Authenticity < 80 | Document review (likely decline) |
| AML confirmed / review | AML compliance review |
| Affordability < 50 | Decline |
Tune for your risk appetite. The Quantum Elixir scores are designed to be calibrated additions to your existing credit model, not replacements.
Wiring with Orchestration
{
"slug": "loan-underwriting-v1",
"spec": {
"startNodeId": "wait_for_uploads",
"nodes": [
{
"id": "wait_for_uploads",
"type": "wait",
"until": "${trigger.lastUploadAt + PT5M}"
},
{
"id": "get_consolidated",
"type": "service_call",
"service": "bank-statement",
"method": "GET",
"path": "/api/consolidations/${trigger.consolidationGroupId}/consolidated"
},
{
"id": "get_npwp",
"type": "service_call",
"service": "document-intelligence",
"method": "GET",
"path": "/api/documents/${trigger.npwpDocId}/extractions"
},
{
"id": "screen_aml",
"type": "service_call",
"service": "aml",
"method": "POST",
"path": "/api/screenings",
"body": {
"lane": "onboarding",
"subjectType": "individual",
"fullName": "${trigger.fullName}",
"nik": "${trigger.nik}",
"externalId": "${trigger.applicationId}"
}
},
{
"id": "underwriting_decision",
"type": "http_request",
"method": "POST",
"url": "${env.CREDIT_MODEL_URL}/decide",
"body": {
"applicationId": "${trigger.applicationId}",
"affordabilityScore": "${step.get_consolidated.output.data.affordabilityScore}",
"averageMonthlyInflow": "${step.get_consolidated.output.data.averageMonthlyInflow}",
"anomalyCount": "${step.get_consolidated.output.data.anomalyCount}",
"amlStatus": "${step.screen_aml.output.data.status}",
"npwpVerified": "${step.get_npwp.output.data[0].fields.npwp_number != null}"
}
}
],
"edges": [
{ "from": "wait_for_uploads", "to": "get_consolidated" },
{ "from": "get_consolidated", "to": "get_npwp" },
{ "from": "get_npwp", "to": "screen_aml" },
{ "from": "screen_aml", "to": "underwriting_decision" }
]
}
}Trigger this workflow when your upload-collection step completes. Subscribe to the workflow.execution.completed webhook to receive the underwriting decision.
Common pitfalls
- Statement period gaps. Customers sometimes upload statements with month-gaps. Check
monthlyBuckets[]for missing months and decline if the gap is too large (typical threshold: > 1 month). - Multi-account masking. A single applicant may funnel earnings across multiple accounts to hide debt service ratios. Consolidating statements across all known accounts (including spouse's) gives a fuller picture.
- Sandbox watchlist hits during QA. The sandbox watchlist contains real public-list entries (UN-SC, OFAC, PPATK PEP). Don't seed your test customers with names like
Pak Chol Ryong,Umar Patek,Vladimir Putin, or NIK3171010101710001(Bambang Setiawan Hartono) unless you're trying to force a confirmed hit — they'll deterministically match.