EnvoyX Docs

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.

Processing Time: Bank statements are typically analyzed in 30-120 seconds depending on document size and complexity. You can track progress in real-time via WebSocket or webhooks.

Overview

The bank statement analysis pipeline:

  1. Extracts metadata (bank name, account number, period, balances, transaction count)
  2. Analyzes across 4 dimensions calibrated to the business type and sector
  3. Scores each dimension (0-1000) and computes an overall credit score
  4. Classifies into credit tiers (Excellent, Good, Fair, Ineligible)
  5. Flags risk indicators with severity levels (critical, high, medium, low)
  6. Verifies document legitimacy and account holder name

API Endpoints

MethodEndpointDescription
POST/api/v1/bank-statements/uploadUpload and analyze a bank statement
GET/api/v1/bank-statementsList 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/preferencesGet your analysis defaults
PUT/api/v1/bank-statements/preferencesUpdate your analysis defaults

Upload Bank Statement

POST https://api.tryenvoyx.com/api/v1/bank-statements/upload
Content-Type: multipart/form-data

Request Fields

FieldTypeRequiredDescription
fileFileYesBank statement file (PDF, JPEG, or PNG, max 20MB)
business_typestringNoBusiness type for calibrated analysis (e.g. healthcare_provider, retail, agriculture)
sectorstringNoSector classification (e.g. healthcare, commerce, services)
account_holder_namestringNoExpected 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

FieldTypeDescription
bank_namestringName of the bank
account_number_maskedstringAccount number with only last 4 digits visible (e.g. ****1234)
statement_period_startstringStart date of the statement period (ISO format)
statement_period_endstringEnd date of the statement period
currencystring3-letter ISO currency code (e.g. XOF)
opening_balancenumberBalance at start of period
closing_balancenumberBalance at end of period
total_creditsnumberSum of all inflows during the period
total_debitsnumberSum of all outflows during the period
transaction_countintegerTotal number of transactions

Account Holder Name Analysis

FieldTypeDescription
extracted_account_holder_namestringName as printed on the statement
account_holder_name_typestringPERSON, BUSINESS, or AMBIGUOUS
account_holder_name_consistencystringCONSISTENT, INCONSISTENT, or UNKNOWN — whether the name appears consistently throughout
name_match_resultstringEXACT, PARTIAL, or MISMATCH — only present if account_holder_name was provided in the upload
name_match_detailsstringExplanation of the match result (e.g. shared tokens, similarity percentage)

Revenue Analysis (Dimension 1)

FieldTypeDescription
revenue_scoreinteger0-1000 score for revenue health
revenue_consistencystringSTRONG, MODERATE, WEAK, or POOR
revenue_classificationobjectPercentage breakdown by inflow type (institutional_payments, mobile_money_sweeps, cash_deposits, tontine_receipts, related_party_transfers, other)
revenue_flagsarrayList of detected revenue risk flags

Cash Flow Health (Dimension 2)

FieldTypeDescription
cashflow_scoreinteger0-1000 score for cash flow health
cashflow_working_capital_adequacystringADEQUATE, MARGINAL, or INADEQUATE
cashflow_liquidity_bridgesobjectDetected liquidity mechanisms (mobile_money_detected, tontine_detected, description)
cashflow_pattern_consistencystringSTRONG, MODERATE, WEAK, or POOR
cashflow_flagsarrayList of detected cash flow risk flags

Expense & Liability (Dimension 3)

FieldTypeDescription
expense_scoreinteger0-1000 score for expense patterns
expense_operating_presencestringPRESENT, PARTIAL, or ABSENT — whether expected business expenses are visible
expense_debt_loadstringLOW, MODERATE, HIGH, or CRITICAL
expense_tontine_activityobjectDetected tontine participation (detected, estimated_monthly, description)
expense_flagsarrayList of detected expense risk flags

Balance Behaviour (Dimension 4)

FieldTypeDescription
balance_scoreinteger0-1000 score for balance patterns
balance_trendstringGROWING, STABLE, DECLINING, or VOLATILE
balance_manipulation_detectedbooleanWhether timing manipulation signals were found
balance_survival_behaviourstringHEALTHY, MARGINAL, or DISTRESSED — how the account survives between major inflows
balance_flagsarrayList of detected balance risk flags

Credit Score & Risk

FieldTypeDescription
credit_scoreintegerOverall credit score (0-1000)
credit_tierstringTIER_1, TIER_2, TIER_3, or INELIGIBLE
overall_risk_levelstringLOW, MEDIUM, HIGH, or CRITICAL
is_legitimatebooleanWhether the document appears to be a genuine bank statement
legitimacy_confidenceintegerConfidence percentage (0-100) for the legitimacy assessment
legitimacy_reasonstringExplanation of the legitimacy determination
all_flagsarrayAggregated list of all flags from all dimensions
flag_countintegerTotal 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:

DimensionWeight
Revenue Analysis30%
Cash Flow Health25%
Expense & Liability25%
Balance Behaviour20%

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

TierScore RangeLabelDescription
TIER_1750-1000ExcellentStrong creditworthiness across all dimensions
TIER_2500-749GoodSolid financial profile with minor concerns
TIER_3250-499FairModerate risk, some flags or weaker dimensions
INELIGIBLE0-249IneligibleHigh risk, critical flags, or illegitimate document

Risk Levels

LevelMeaning
LOWScore 750+, no critical flags
MEDIUMScore 500-749, no critical flags
HIGHScore 250-499, or critical flags present
CRITICALScore 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:

StepDescription
1Starting bank statement analysis
2Downloading statement from storage
3Validating document format
4Analyzing bank statement with AI
5Processing account holder information
6Evaluating revenue patterns
7Assessing cash flow health
8Analyzing expense & liability patterns
9Evaluating balance behaviour
10Computing credit score
11Checking for duplicate submissions
12Finalizing 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-statements

Query Parameters

ParameterTypeDefaultDescription
pageinteger1Page number
page_sizeinteger20Results per page (max 100)
status_filterstringFilter by status (PENDING, PROCESSING, ANALYZED, FLAGGED, FAILED)
credit_tier_filterstringFilter 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:

  1. Request parameter — if provided in the upload request, it's used
  2. Saved preference — if not in the request, your saved default is used
  3. System default — if no preference is saved, the system default applies

Get Preferences

GET https://api.tryenvoyx.com/api/v1/bank-statements/preferences

Update 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:

ResultMeaning
EXACTNames match exactly (after normalization)
PARTIALNames partially overlap (shared tokens above 60% similarity)
MISMATCHNames 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.

EventDescription
bank_statement.uploadedStatement file uploaded (async mode only)
bank_statement.processingAnalysis in progress with step details
bank_statement.analyzedAnalysis complete, no critical flags
bank_statement.flaggedAnalysis complete, flags detected
bank_statement.failedAnalysis 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.

On this page