← Back to blog
·9 min read

Parsing Union Bank of India Statements with AI

Extract transactions, balances, and cash flow from Union Bank of India statements using AI. Includes TypeScript code, quirks to handle, and production tips.

union bank of indiabank statement parserai extractionfintech apiindian bankingdocument intelligenceloan underwriting

Union Bank of India is the fifth-largest public sector bank in India — formed in 2020 after the merger of Andhra Bank and Corporation Bank into the original Union Bank. With over 120 million customers and 9,500+ branches, its statements appear constantly in loan applications, KYC flows, and financial health checks.

If you're building an AI agent that processes Indian bank statements, Union Bank is one you cannot skip. This guide covers how Lekha extracts data from Union Bank statements, the quirks you'll encounter, and production-ready TypeScript code.


What Makes Union Bank Statements Distinctive

Union Bank statements come in two flavours depending on how the customer downloads them:

  • Internet Banking PDF — the classic format with a header section showing account details followed by a tabular transaction ledger.
  • Mobile Banking PDF (VYOM app) — a more compact layout that compresses the transaction table and occasionally uses abbreviated narrations.
  • The merger also introduced a transitional period where some branches still issued statements with Andhra Bank or Corporation Bank branding in the header — despite the underlying account being a Union Bank account. A naive parser that checks the bank name in the header will misclassify these.

    Other quirks:

  • Narration truncation — descriptions longer than ~50 characters are cut off. UPI references and NEFT/RTGS senders often lose their trailing identifiers.
  • Balance-forward rows — statements spanning multiple months include an opening "Balance B/F" row that is not a real transaction.
  • Cheque number column — present but frequently blank for digital transactions; should be treated as nullable, not zero.
  • Date format — DD-MM-YYYY with a hyphen separator, not a slash. Parsers expecting DD/MM/YYYY will fail silently.

  • Quick Start with the Lekha API

    The simplest way to extract a Union Bank statement is a single API call:

    import fs from "fs";
    

    const file = fs.readFileSync("union-bank-statement.pdf"); const base64 = file.toString("base64");

    const response = await fetch("https://lekhadev.com/api/v1/extract", { method: "POST", headers: { Authorization: Bearer ${process.env.LEKHA_API_KEY}, "Content-Type": "application/json", }, body: JSON.stringify({ document: base64, type: "bank_statement", }), });

    const { data } = await response.json();

    console.log(data.bank_name); // "Union Bank of India" console.log(data.account_number); // "XXXXXXXXXX1234" console.log(data.transactions); // Array of transaction objects

    Lekha returns a normalised JSON schema regardless of which Union Bank format the PDF uses. You don't need to branch on internet-banking vs VYOM — the classifier handles it.


    The Full Extracted Schema

    A successful extraction returns:

    interface UnionBankStatement {
      bank_name: string; // "Union Bank of India"
      account_number: string; // masked or full depending on PDF
      account_holder: string;
      ifsc: string;
      branch: string;
      account_type: string; // "Savings", "Current", "OD"
      currency: string; // "INR"
      statement_period: {
        from: string; // ISO 8601: "2026-01-01"
        to: string; // ISO 8601: "2026-06-30"
      };
      opening_balance: number;
      closing_balance: number;
      total_credits: number;
      total_debits: number;
      transactions: Transaction[];
    }
    

    interface Transaction { date: string; // ISO 8601 description: string; cheque_number: string | null; debit: number | null; credit: number | null; balance: number; transaction_type: string; // "UPI", "NEFT", "IMPS", "ATM", "CHEQUE", ... reference_number: string | null; }

    Note that amounts are always numbers, never strings — 12500 not "12,500.00".


    Handling the Merger Legacy: Multi-Brand Statements

    If your pipeline processes statements from older Union Bank customers, you may receive PDFs that show "Andhra Bank" or "Corporation Bank" in the header, with a Union Bank IFSC code (starting with UBIN).

    The right approach is to classify by IFSC, not by header text. Lekha does this automatically, but if you're building a fallback classifier yourself:

    function classifyUnionBankStatement(ifsc: string, headerText: string): boolean {
      // Post-merger IFSCs all start with UBIN
      if (ifsc.startsWith("UBIN")) return true;
    

    // Catch legacy branding during transitional period const legacyBrands = ["union bank", "andhra bank", "corporation bank"]; return legacyBrands.some((brand) => headerText.toLowerCase().includes(brand)); }

    This prevents false negatives when a customer downloads an old Andhra Bank–branded PDF that is genuinely a Union Bank account.


    Building a Cash Flow Analyser

    Once you have the transaction array, you can derive higher-level signals. Here's a lightweight cash flow analysis function useful in loan underwriting or expense audits:

    interface CashFlowSummary {
      averageMonthlyCredit: number;
      averageMonthlyDebit: number;
      salaryCredits: Transaction[];
      upiDebits: number;
      atmWithdrawals: number;
      emiDebits: Transaction[];
      monthlyNetFlow: Record;
    }
    

    function analyseCashFlow( transactions: Transaction[], statementPeriodMonths: number, ): CashFlowSummary { const salaryKeywords = ["salary", "sal", "payroll", "pay"]; const emiKeywords = ["emi", "loan", "installment", "equated"];

    const salaryCredits = transactions.filter( (t) => t.credit !== null && salaryKeywords.some((kw) => t.description.toLowerCase().includes(kw)), );

    const emiDebits = transactions.filter( (t) => t.debit !== null && emiKeywords.some((kw) => t.description.toLowerCase().includes(kw)), );

    const totalCredits = transactions.reduce( (sum, t) => sum + (t.credit ?? 0), 0, ); const totalDebits = transactions.reduce((sum, t) => sum + (t.debit ?? 0), 0);

    const upiDebits = transactions .filter((t) => t.transaction_type === "UPI" && t.debit !== null) .reduce((sum, t) => sum + (t.debit ?? 0), 0);

    const atmWithdrawals = transactions .filter((t) => t.transaction_type === "ATM") .reduce((sum, t) => sum + (t.debit ?? 0), 0);

    // Group net flow by month const monthlyNetFlow: Record = {}; for (const t of transactions) { const month = t.date.substring(0, 7); // "YYYY-MM" monthlyNetFlow[month] ??= 0; monthlyNetFlow[month] += (t.credit ?? 0) - (t.debit ?? 0); }

    return { averageMonthlyCredit: totalCredits / statementPeriodMonths, averageMonthlyDebit: totalDebits / statementPeriodMonths, salaryCredits, upiDebits, atmWithdrawals, emiDebits, monthlyNetFlow, }; }


    Processing Multiple Months in Sequence

    Union Bank customers applying for a home loan or business loan are often asked for 6–12 months of statements. They download them one month at a time and upload multiple PDFs. Your pipeline needs to handle deduplication and sequencing.

    async function processUnionBankStatements(pdfs: Buffer[]) {
      const extractions = await Promise.all(
        pdfs.map(async (pdf) => {
          const response = await fetch("https://lekhadev.com/api/v1/extract", {
            method: "POST",
            headers: {
              Authorization: Bearer ${process.env.LEKHA_API_KEY},
              "Content-Type": "application/json",
            },
            body: JSON.stringify({
              document: pdf.toString("base64"),
              type: "bank_statement",
            }),
          });
          const { data } = await response.json();
          return data;
        }),
      );
    

    // Sort by statement start date extractions.sort( (a, b) => new Date(a.statement_period.from).getTime() - new Date(b.statement_period.from).getTime(), );

    // Deduplicate transactions across overlapping periods const seenRefs = new Set(); const allTransactions = extractions .flatMap((e) => e.transactions) .filter((t) => { const key = ${t.date}|${t.debit ?? t.credit}|${t.description.slice(0, 30)}; if (seenRefs.has(key)) return false; seenRefs.add(key); return true; });

    return { accountNumber: extractions[0].account_number, combinedPeriod: { from: extractions[0].statement_period.from, to: extractions[extractions.length - 1].statement_period.to, }, transactions: allTransactions, }; }

    The deduplication key uses date + amount + the first 30 characters of the narration. This catches the "Balance B/F" rows that appear at the start of each monthly statement.


    Loan Underwriting: Minimum Viable Check

    Here's a simple underwriting readiness check you can run on the extracted data before forwarding to a credit team:

    interface UnderwritingCheck {
      hasSteadySalary: boolean;
      averageSalary: number;
      debtServiceRatio: number; // EMI burden / average monthly credit
      hasBouncedCheques: boolean;
      minimumBalance: number;
    }
    

    function underwritingCheck( transactions: Transaction[], monthCount: number, ): UnderwritingCheck { const salaryKeywords = ["salary", "sal ", "payroll"]; const bounceKeywords = ["bounced", "dishonour", "returned", "insufficient"];

    const salaryTxns = transactions.filter( (t) => t.credit !== null && salaryKeywords.some((kw) => t.description.toLowerCase().includes(kw)), );

    const emiTxns = transactions.filter( (t) => t.debit !== null && ["emi", "loan"].some((kw) => t.description.toLowerCase().includes(kw)), );

    const totalSalary = salaryTxns.reduce((s, t) => s + (t.credit ?? 0), 0); const totalEmi = emiTxns.reduce((s, t) => s + (t.debit ?? 0), 0); const totalCredits = transactions.reduce((s, t) => s + (t.credit ?? 0), 0);

    const avgMonthlyCredit = totalCredits / monthCount; const avgMonthlyEmi = totalEmi / monthCount;

    const bounces = transactions.some((t) => bounceKeywords.some((kw) => t.description.toLowerCase().includes(kw)), );

    const minBalance = Math.min(...transactions.map((t) => t.balance));

    return { hasSteadySalary: salaryTxns.length >= monthCount * 0.8, averageSalary: totalSalary / Math.max(salaryTxns.length, 1), debtServiceRatio: avgMonthlyEmi / avgMonthlyCredit, hasBouncedCheques: bounces, minimumBalance: minBalance, }; }

    A debt service ratio below 0.4 and no bounced cheques is a common starting threshold for personal loan pre-qualification.


    Common Errors and How to Fix Them

    | Error | Cause | Fix | | -------------------------- | ------------------------------------------- | ----------------------------------------------------- | | bank_name: null | Andhra Bank legacy header | Re-classify by IFSC prefix UBIN | | Transactions off by 1 day | DD-MM-YYYY parsed as MM-DD-YYYY | Lekha normalises to ISO 8601 — verify your own parser | | Balance jumps unexpectedly | "Balance B/F" row included as a transaction | Filter rows where description contains "B/F" | | Missing narration detail | VYOM app truncates to 50 chars | Store raw description; don't rely on full sender name | | Duplicate transactions | Overlapping monthly statements | Deduplicate by date + amount + narration prefix |


    FAQ

    Does Lekha support the VYOM mobile banking PDF format? Yes. The classifier detects both the internet banking and VYOM app layouts and applies the appropriate extraction strategy. You send the same API request for both. How does Lekha handle Andhra Bank and Corporation Bank legacy PDFs? Lekha classifies Union Bank statements by IFSC code (UBIN prefix) in addition to header text, so legacy-branded PDFs from merged entities are correctly identified. Can I extract statements from the Union Bank Vyom app without passwords? Union Bank allows customers to download statements without a PDF password from internet banking. Password-protected PDFs (typically set to the customer's date of birth) require the password to be provided; Lekha accepts an optional pdf_password field in the request body. What is the extraction accuracy for Union Bank statements? In Lekha's benchmark set, Union Bank statements achieve >99% transaction-level accuracy across both the internet banking and VYOM formats, with the main variance being in highly truncated UPI narrations.

    Next Steps

    Union Bank statement parsing is one API call with Lekha. From there, you can:

  • Try it live in the Lekha Playground — upload a statement and see the JSON in seconds.
  • Read the full API docs for all supported document types.
  • Combine bank statement data with salary slip extraction or CAS statements for a complete financial picture.
  • Ready to build? Sign up at lekhadev.com — the free plan includes 50 extractions per month with no credit card required.