Bank Statements
Bank Statement Analysis
Upload bank statements to get AI-powered creditworthiness analysis across four dimensions — Revenue, Cash Flow, Expense & Liability, and Balance Behaviour. Each statement receives a credit score (0-1000), tier classification, risk level, and detailed flags.
Overview
The bank statement analysis pipeline:
- Extracts metadata (bank name, account number, period, balances, transaction count)
- Analyzes across 4 dimensions calibrated to the business type and sector
- Scores each dimension (0-1000) and computes an overall credit score
- Classifies into credit tiers (Excellent, Good, Fair, Ineligible)
- Flags risk indicators with severity levels (critical, high, medium, low)
- Verifies document legitimacy and account holder name
API Endpoints
| Method | Endpoint | Description |
|---|---|---|
POST | /api/v1/bank-statements/upload | Upload and analyze a bank statement |
GET | /api/v1/bank-statements | List bank statements with pagination |
GET | /api/v1/bank-statements/{id} | Get full analysis details |
DELETE | /api/v1/bank-statements/{id} | Delete a bank statement |
GET | /api/v1/bank-statements/preferences | Get your analysis defaults |
PUT | /api/v1/bank-statements/preferences | Update your analysis defaults |
Upload Bank Statement
POST https://api.tryenvoyx.com/api/v1/bank-statements/upload
Content-Type: multipart/form-dataRequest Fields
| Field | Type | Required | Description |
|---|---|---|---|
file | File | Yes | Bank statement file (PDF, JPEG, or PNG, max 20MB) |
business_type | string | No | Business type for calibrated analysis (e.g. healthcare_provider, retail, agriculture) |
sector | string | No | Sector classification (e.g. healthcare, commerce, services) |
account_holder_name | string | No | Expected account holder name for comparison matching |
Parameter Priority: If business_type or sector is not provided in the request, the system checks your saved preferences (see Preferences API). If no preferences are saved, system defaults are used.
Streaming Support
Add the X-Stream-Response: true header to receive real-time Server-Sent Events (SSE) as the analysis progresses through all 12 steps. Without this header, the request returns immediately and processing happens asynchronously — you'll receive updates via webhooks or WebSocket.
Code Examples
curl -X POST https://api.tryenvoyx.com/api/v1/bank-statements/upload \
-H "X-API-Key: YOUR_API_KEY" \
-F "file=@bank_statement.pdf" \
-F "business_type=healthcare_provider" \
-F "sector=healthcare" \
-F "account_holder_name=CLINIQUE SAINTE MARIE SARL"const formData = new FormData()
formData.append('file', fs.createReadStream('bank_statement.pdf'))
formData.append('business_type', 'healthcare_provider')
formData.append('sector', 'healthcare')
formData.append('account_holder_name', 'CLINIQUE SAINTE MARIE SARL')
const response = await fetch('https://api.tryenvoyx.com/api/v1/bank-statements/upload', {
method: 'POST',
headers: { 'X-API-Key': 'YOUR_API_KEY' },
body: formData,
})
const { data } = await response.json()
console.log('Credit Score:', data.bank_statement.credit_score)
console.log('Tier:', data.bank_statement.credit_tier)import requests
with open('bank_statement.pdf', 'rb') as f:
response = requests.post(
'https://api.tryenvoyx.com/api/v1/bank-statements/upload',
headers={'X-API-Key': 'YOUR_API_KEY'},
files={'file': ('bank_statement.pdf', f, 'application/pdf')},
data={
'business_type': 'healthcare_provider',
'sector': 'healthcare',
'account_holder_name': 'CLINIQUE SAINTE MARIE SARL',
},
)
result = response.json()['data']['bank_statement']
print(f"Credit Score: {result['credit_score']}")
print(f"Tier: {result['credit_tier']}")
print(f"Risk Level: {result['overall_risk_level']}")Upload Response
{
"success": true,
"status": 201,
"message": "Bank statement uploaded successfully. Analysis started.",
"data": {
"bank_statement": {
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"filename": "bank_statement.pdf",
"original_name": "bank_statement.pdf",
"file_type": "application/pdf",
"file_size": 14952,
"status": "PENDING",
"business_type": "healthcare_provider",
"sector": "healthcare",
"credit_score": null,
"credit_tier": null,
"created_at": "2026-04-07T10:30:00.000Z",
"updated_at": "2026-04-07T10:30:00.000Z",
"download_url": "https://..."
}
}
}Analysis Response Fields
Once analysis completes, the bank statement object contains detailed results across all dimensions.
Statement Metadata
| Field | Type | Description |
|---|---|---|
bank_name | string | Name of the bank |
account_number_masked | string | Account number with only last 4 digits visible (e.g. ****1234) |
statement_period_start | string | Start date of the statement period (ISO format) |
statement_period_end | string | End date of the statement period |
currency | string | 3-letter ISO currency code (e.g. XOF) |
opening_balance | number | Balance at start of period |
closing_balance | number | Balance at end of period |
total_credits | number | Sum of all inflows during the period |
total_debits | number | Sum of all outflows during the period |
transaction_count | integer | Total number of transactions |
Account Holder Name Analysis
| Field | Type | Description |
|---|---|---|
extracted_account_holder_name | string | Name as printed on the statement |
account_holder_name_type | string | PERSON, BUSINESS, or AMBIGUOUS |
account_holder_name_consistency | string | CONSISTENT, INCONSISTENT, or UNKNOWN — whether the name appears consistently throughout |
name_match_result | string | EXACT, PARTIAL, or MISMATCH — only present if account_holder_name was provided in the upload |
name_match_details | string | Explanation of the match result (e.g. shared tokens, similarity percentage) |
Revenue Analysis (Dimension 1)
| Field | Type | Description |
|---|---|---|
revenue_score | integer | 0-1000 score for revenue health |
revenue_consistency | string | STRONG, MODERATE, WEAK, or POOR |
revenue_classification | object | Percentage breakdown by inflow type (institutional_payments, mobile_money_sweeps, cash_deposits, tontine_receipts, related_party_transfers, other) |
revenue_flags | array | List of detected revenue risk flags |
Cash Flow Health (Dimension 2)
| Field | Type | Description |
|---|---|---|
cashflow_score | integer | 0-1000 score for cash flow health |
cashflow_working_capital_adequacy | string | ADEQUATE, MARGINAL, or INADEQUATE |
cashflow_liquidity_bridges | object | Detected liquidity mechanisms (mobile_money_detected, tontine_detected, description) |
cashflow_pattern_consistency | string | STRONG, MODERATE, WEAK, or POOR |
cashflow_flags | array | List of detected cash flow risk flags |
Expense & Liability (Dimension 3)
| Field | Type | Description |
|---|---|---|
expense_score | integer | 0-1000 score for expense patterns |
expense_operating_presence | string | PRESENT, PARTIAL, or ABSENT — whether expected business expenses are visible |
expense_debt_load | string | LOW, MODERATE, HIGH, or CRITICAL |
expense_tontine_activity | object | Detected tontine participation (detected, estimated_monthly, description) |
expense_flags | array | List of detected expense risk flags |
Balance Behaviour (Dimension 4)
| Field | Type | Description |
|---|---|---|
balance_score | integer | 0-1000 score for balance patterns |
balance_trend | string | GROWING, STABLE, DECLINING, or VOLATILE |
balance_manipulation_detected | boolean | Whether timing manipulation signals were found |
balance_survival_behaviour | string | HEALTHY, MARGINAL, or DISTRESSED — how the account survives between major inflows |
balance_flags | array | List of detected balance risk flags |
Credit Score & Risk
| Field | Type | Description |
|---|---|---|
credit_score | integer | Overall credit score (0-1000) |
credit_tier | string | TIER_1, TIER_2, TIER_3, or INELIGIBLE |
overall_risk_level | string | LOW, MEDIUM, HIGH, or CRITICAL |
is_legitimate | boolean | Whether the document appears to be a genuine bank statement |
legitimacy_confidence | integer | Confidence percentage (0-100) for the legitimacy assessment |
legitimacy_reason | string | Explanation of the legitimacy determination |
all_flags | array | Aggregated list of all flags from all dimensions |
flag_count | integer | Total number of flags |
Understanding the Four Dimensions
Each dimension evaluates a specific aspect of creditworthiness. The AI calibrates its assessment based on the business_type and sector you provide — a healthcare clinic has different expected patterns than a retail store.
1. Revenue Analysis
What it evaluates: All credit transactions (inflows) are classified by type — institutional payments, mobile money sweeps, cash deposits, tontine receipts, and related-party transfers. Revenue patterns are assessed against what's typical for the declared business type and sector.
Key indicators:
- Revenue consistency and diversity of income sources
- Whether revenue patterns match the declared sector
- Trend direction (increasing, stable, declining, volatile)
Common flags:
- Round-number deposits — Suspicious pattern of exact round amounts (e.g. exactly 1,000,000 FCFA)
- Coordinated inflows — Multiple deposits from different sources on the same day
- Revenue only near application date — Income appearing only at the end of the statement period (padding)
- Inflows inconsistent with declared sector — Revenue patterns don't match the stated business type
2. Cash Flow Health
What it evaluates: How the business manages the gap between major inflows — the real stress test in markets with slow institutional payers. This dimension looks at working capital behaviour during dry periods and identifies liquidity bridge mechanisms.
Key indicators:
- Working capital adequacy during gaps between payments
- Use of mobile money and tontine as liquidity bridges
- Pattern consistency over the full statement period
Common flags:
- Pass-through behaviour — Money comes in and immediately goes out (no operating balance)
- Circular flows — Transfers between related accounts that inflate activity
- Single related-party dependency — Cash flow entirely dependent on one related-party transfer
3. Expense & Liability Patterns
What it evaluates: Whether the expected operating expenses for the declared business type are present. A real healthcare clinic should show rent, salaries, utilities, medical supply purchases. This dimension also assesses debt load and separates tontine from formal debt.
Key indicators:
- Presence of expected operating expenses
- Debt service ratio and number of visible lenders
- Personal vs operational expense balance
Common flags:
- Absence of operating expenses — "Ghost business" indicator: no rent, no salaries, no supplies
- Loan stacking — Debt service payments to multiple lenders simultaneously
- Large cash withdrawals after deposits — Money deposited then immediately withdrawn in cash
4. Balance Behaviour
What it evaluates: Balance patterns for evidence of timing manipulation and the account's survival behaviour between major inflows. Also evaluates the balance trend over the full statement period.
Key indicators:
- Balance trend direction and stability
- Survival balance between major inflows
- Account age relative to activity volume
Common flags:
- End-of-period spikes — Balance inflated just before the statement date, inconsistent with prior months
- Artificially stable balances — Suspiciously consistent balance that doesn't fluctuate naturally
- Account age too short — High volume of activity in an account that's too new
Credit Score & Tiers
Score Calculation
The overall credit score is a weighted combination of the four dimension scores:
| Dimension | Weight |
|---|---|
| Revenue Analysis | 30% |
| Cash Flow Health | 25% |
| Expense & Liability | 25% |
| Balance Behaviour | 20% |
The base score is then adjusted:
- Flag penalties: Each flag deducts points based on severity — critical flags deduct 100 points, high flags 50, medium 25, low 10
- Legitimacy gate: If the document is determined to not be legitimate (
is_legitimate: false), the score is capped at 200 regardless of dimension scores
Credit Tiers
| Tier | Score Range | Label | Description |
|---|---|---|---|
TIER_1 | 750-1000 | Excellent | Strong creditworthiness across all dimensions |
TIER_2 | 500-749 | Good | Solid financial profile with minor concerns |
TIER_3 | 250-499 | Fair | Moderate risk, some flags or weaker dimensions |
INELIGIBLE | 0-249 | Ineligible | High risk, critical flags, or illegitimate document |
Risk Levels
| Level | Meaning |
|---|---|
LOW | Score 750+, no critical flags |
MEDIUM | Score 500-749, no critical flags |
HIGH | Score 250-499, or critical flags present |
CRITICAL | Score below 250, or document not legitimate |
Status Lifecycle
PENDING → PROCESSING → ANALYZED (no critical flags, legitimate)
→ FLAGGED (critical flags detected or not legitimate)
→ FAILED (processing error)- PENDING — Uploaded, waiting for processing
- PROCESSING — AI analysis in progress (12 steps)
- ANALYZED — Successfully analyzed with no critical issues
- FLAGGED — Analyzed but critical flags were detected
- FAILED — An error occurred during analysis
Processing Pipeline
The analysis runs through 12 sequential steps. Each step emits a progress event if you're using WebSocket or webhooks:
| Step | Description |
|---|---|
| 1 | Starting bank statement analysis |
| 2 | Downloading statement from storage |
| 3 | Validating document format |
| 4 | Analyzing bank statement with AI |
| 5 | Processing account holder information |
| 6 | Evaluating revenue patterns |
| 7 | Assessing cash flow health |
| 8 | Analyzing expense & liability patterns |
| 9 | Evaluating balance behaviour |
| 10 | Computing credit score |
| 11 | Checking for duplicate submissions |
| 12 | Finalizing analysis results |
Step 4 (AI analysis) is the longest step — this is where the Gemini AI processes the full document. Steps 5-12 are fast post-processing of the AI results.
List Bank Statements
GET https://api.tryenvoyx.com/api/v1/bank-statementsQuery Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
page | integer | 1 | Page number |
page_size | integer | 20 | Results per page (max 100) |
status_filter | string | — | Filter by status (PENDING, PROCESSING, ANALYZED, FLAGGED, FAILED) |
credit_tier_filter | string | — | Filter by credit tier (TIER_1, TIER_2, TIER_3, INELIGIBLE) |
Response
{
"success": true,
"data": {
"bank_statements": [
{
"id": "a1b2c3d4-...",
"status": "ANALYZED",
"bank_name": "BANQUE ATLANTIQUE COTE D'IVOIRE",
"credit_score": 723,
"credit_tier": "TIER_2",
"overall_risk_level": "MEDIUM",
"flag_count": 2,
"created_at": "2026-04-07T10:30:00.000Z",
"download_url": "https://..."
}
],
"total": 15,
"page": 1,
"page_size": 20
}
}Get Bank Statement Detail
GET https://api.tryenvoyx.com/api/v1/bank-statements/{id}Returns the full analysis including all dimension scores, flags, metadata, name analysis, and the raw AI analysis data.
curl https://api.tryenvoyx.com/api/v1/bank-statements/a1b2c3d4-e5f6-7890-abcd-ef1234567890 \
-H "X-API-Key: YOUR_API_KEY"const response = await fetch(
'https://api.tryenvoyx.com/api/v1/bank-statements/a1b2c3d4-e5f6-7890-abcd-ef1234567890',
{ headers: { 'X-API-Key': 'YOUR_API_KEY' } }
)
const { data } = await response.json()
const stmt = data.bank_statement
console.log(`Score: ${stmt.credit_score} (${stmt.credit_tier})`)
console.log(`Revenue: ${stmt.revenue_score}, Cash Flow: ${stmt.cashflow_score}`)
console.log(`Expense: ${stmt.expense_score}, Balance: ${stmt.balance_score}`)
console.log(`Flags: ${stmt.all_flags?.join(', ')}`)import requests
response = requests.get(
'https://api.tryenvoyx.com/api/v1/bank-statements/a1b2c3d4-e5f6-7890-abcd-ef1234567890',
headers={'X-API-Key': 'YOUR_API_KEY'},
)
stmt = response.json()['data']['bank_statement']
print(f"Score: {stmt['credit_score']} ({stmt['credit_tier']})")
print(f"Revenue: {stmt['revenue_score']}, Cash Flow: {stmt['cashflow_score']}")
print(f"Flags: {stmt['all_flags']}")Delete Bank Statement
DELETE https://api.tryenvoyx.com/api/v1/bank-statements/{id}Permanently deletes the bank statement, its analysis data, and the stored file.
Preferences API
Set default business_type and sector so you don't have to pass them with every upload. The priority chain is:
- Request parameter — if provided in the upload request, it's used
- Saved preference — if not in the request, your saved default is used
- System default — if no preference is saved, the system default applies
Get Preferences
GET https://api.tryenvoyx.com/api/v1/bank-statements/preferencesUpdate Preferences
curl -X PUT https://api.tryenvoyx.com/api/v1/bank-statements/preferences \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"default_business_type": "healthcare_provider", "default_sector": "healthcare"}'const response = await fetch('https://api.tryenvoyx.com/api/v1/bank-statements/preferences', {
method: 'PUT',
headers: {
'X-API-Key': 'YOUR_API_KEY',
'Content-Type': 'application/json',
},
body: JSON.stringify({
default_business_type: 'healthcare_provider',
default_sector: 'healthcare',
}),
})response = requests.put(
'https://api.tryenvoyx.com/api/v1/bank-statements/preferences',
headers={'X-API-Key': 'YOUR_API_KEY'},
json={
'default_business_type': 'healthcare_provider',
'default_sector': 'healthcare',
},
)Account Holder Name Comparison
If you provide the account_holder_name parameter during upload, the system compares it against the name extracted from the bank statement:
| Result | Meaning |
|---|---|
EXACT | Names match exactly (after normalization) |
PARTIAL | Names partially overlap (shared tokens above 60% similarity) |
MISMATCH | Names do not match |
The comparison normalizes for case, accents, and punctuation. Business entity suffixes (SARL, SA, SAS, EURL, etc.) are recognized for accurate classification.
Name comparison is optional. If account_holder_name is not provided, the name is still extracted and analyzed (type classification, consistency check) — only the comparison match is skipped.
Webhook Events
Bank statement analysis emits the following webhook events. See the Webhooks guide for setup details.
| Event | Description |
|---|---|
bank_statement.uploaded | Statement file uploaded (async mode only) |
bank_statement.processing | Analysis in progress with step details |
bank_statement.analyzed | Analysis complete, no critical flags |
bank_statement.flagged | Analysis complete, flags detected |
bank_statement.failed | Analysis error |
Events use bank_statement_id instead of invoice_id in the payload:
{
"event": "bank_statement.analyzed",
"timestamp": "2026-04-07T10:31:45.000Z",
"bank_statement_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"step": 12,
"total_steps": 12,
"message": "Bank statement analysis complete.",
"data": {
"status": "ANALYZED",
"credit_score": 723,
"credit_tier": "TIER_2",
"overall_risk_level": "MEDIUM",
"is_legitimate": true,
"flag_count": 2,
"processing_time": 45.3
}
}Complete Integration Example
Upload a bank statement and poll for results:
async function analyzeBankStatement(filePath, apiKey) {
// 1. Upload
const formData = new FormData()
formData.append('file', fs.createReadStream(filePath))
formData.append('business_type', 'healthcare_provider')
const upload = await fetch('https://api.tryenvoyx.com/api/v1/bank-statements/upload', {
method: 'POST',
headers: { 'X-API-Key': apiKey },
body: formData,
})
const { data } = await upload.json()
const id = data.bank_statement.id
console.log(`Uploaded: ${id}`)
// 2. Poll until complete
while (true) {
await new Promise(r => setTimeout(r, 5000)) // wait 5s
const res = await fetch(`https://api.tryenvoyx.com/api/v1/bank-statements/${id}`, {
headers: { 'X-API-Key': apiKey },
})
const result = res.json()
const stmt = result.data.bank_statement
if (['ANALYZED', 'FLAGGED', 'FAILED'].includes(stmt.status)) {
console.log(`Status: ${stmt.status}`)
console.log(`Credit Score: ${stmt.credit_score} (${stmt.credit_tier})`)
console.log(`Risk: ${stmt.overall_risk_level}`)
console.log(`Revenue: ${stmt.revenue_score}, Cash Flow: ${stmt.cashflow_score}`)
console.log(`Expense: ${stmt.expense_score}, Balance: ${stmt.balance_score}`)
if (stmt.all_flags?.length) {
console.log(`Flags: ${stmt.all_flags.join(', ')}`)
}
return stmt
}
console.log(`Status: ${stmt.status} - waiting...`)
}
}import requests
import time
def analyze_bank_statement(file_path, api_key):
headers = {'X-API-Key': api_key}
# 1. Upload
with open(file_path, 'rb') as f:
response = requests.post(
'https://api.tryenvoyx.com/api/v1/bank-statements/upload',
headers=headers,
files={'file': f},
data={'business_type': 'healthcare_provider'},
)
stmt_id = response.json()['data']['bank_statement']['id']
print(f'Uploaded: {stmt_id}')
# 2. Poll until complete
while True:
time.sleep(5)
response = requests.get(
f'https://api.tryenvoyx.com/api/v1/bank-statements/{stmt_id}',
headers=headers,
)
stmt = response.json()['data']['bank_statement']
if stmt['status'] in ('ANALYZED', 'FLAGGED', 'FAILED'):
print(f"Status: {stmt['status']}")
print(f"Credit Score: {stmt['credit_score']} ({stmt['credit_tier']})")
print(f"Risk: {stmt['overall_risk_level']}")
print(f"Revenue: {stmt['revenue_score']}, Cash Flow: {stmt['cashflow_score']}")
print(f"Expense: {stmt['expense_score']}, Balance: {stmt['balance_score']}")
if stmt.get('all_flags'):
print(f"Flags: {', '.join(stmt['all_flags'])}")
return stmt
print(f"Status: {stmt['status']} - waiting...")Tip: For production integrations, use webhooks instead of polling. Subscribe to bank_statement.analyzed and bank_statement.flagged events to receive results as soon as they're ready.