← Back to blog
·9 min read

Build a Home Loan Pre-Qualification Agent with Lekha

Build an AI agent that pre-qualifies home loan applicants by parsing salary slips and bank statements to calculate FOIR and eligibility in seconds.

home loanai agentsalary slipbank statementFOIRlendingfintech indiadocument extraction

Home loan pre-qualification is one of the most document-heavy workflows in Indian banking. A typical applicant submits 3–6 months of bank statements, the last 3 salary slips, Form 16, and sometimes an ITR — all of which a loan officer then manually keys into a spreadsheet to compute FOIR (Fixed Obligation to Income Ratio) and net take-home pay.

This guide shows you how to build an AI agent that does this in seconds. The agent accepts documents, extracts the financial data, and returns a structured pre-qualification verdict — ready to plug into your lending workflow.

What the Agent Does

The home loan pre-qualification agent:

  • Accepts salary slips (PDF or image) for the last 3 months
  • Accepts bank statements for the last 6 months
  • Extracts net monthly income, recurring EMI debits, and average monthly balance
  • Calculates FOIR and eligible loan amount using standard RBI lending guidelines
  • Returns a structured JSON verdict with a pass/fail and the supporting numbers
  • FOIR is the key underwriting metric for Indian home loans. Most lenders approve borrowers with FOIR ≤ 50% (i.e., existing loan EMIs must not exceed 50% of gross income). The agent computes this automatically.

    Prerequisites

    bun add lekha-sdk
    

    You'll need a Lekha API key from lekhadev.com. Set it as LEKHA_API_KEY in your environment.

    Step 1: Extract Salary Data

    Start by extracting the net monthly salary from each pay slip. Lekha returns a normalized SalarySlip object regardless of whether the employer uses Greytip, Keka, Darwinbox, or a custom payroll format.

    import Lekha from "lekha-sdk";
    import { readFileSync } from "fs";
    

    const lekha = new Lekha({ apiKey: process.env.LEKHA_API_KEY });

    interface SalaryData { grossSalary: number; netSalary: number; month: string; employerName: string; pfDeduction: number; }

    async function extractSalarySlips(paths: string[]): Promise { const results = await Promise.all( paths.map(async (path) => { const file = readFileSync(path); const result = await lekha.extract({ document: file, type: "salary_slip", });

    if (!result.success) { throw new Error(Failed to parse ${path}: ${result.error.message}); }

    return { grossSalary: result.data.gross_salary, netSalary: result.data.net_salary, month: result.data.pay_period, employerName: result.data.employer_name, pfDeduction: result.data.pf_employee_contribution ?? 0, }; }), );

    return results; }

    Lekha handles the quirks of Indian payslips automatically — split pay periods, arrears, variable components, and LTA. You get clean numbers without any regex.

    Step 2: Extract Bank Statement Debits

    Next, parse the bank statements to identify recurring EMI debits. These are what go into the FOIR numerator. Lekha classifies transactions and flags regular outflows (typically NACH debits, auto-pay, and standing instructions).

    interface BankData {
      averageMonthlyBalance: number;
      recurringDebits: RecurringDebit[];
      salaryCredits: SalaryCredit[];
    }
    

    interface RecurringDebit { description: string; amount: number; frequency: "monthly" | "quarterly"; category: "emi" | "insurance" | "subscription" | "other"; }

    interface SalaryCredit { amount: number; date: string; narration: string; }

    async function extractBankStatements(paths: string[]): Promise { const allTransactions: unknown[] = []; let totalClosingBalance = 0;

    for (const path of paths) { const file = readFileSync(path); const result = await lekha.extract({ document: file, type: "bank_statement", });

    if (!result.success) { throw new Error(Failed to parse ${path}: ${result.error.message}); }

    allTransactions.push(...result.data.transactions); totalClosingBalance += result.data.closing_balance ?? 0; }

    // Lekha enriches transactions with category and recurring flag const recurringDebits = ( allTransactions as Array<{ type: string; is_recurring: boolean; amount: number; narration: string; category: string; }> ) .filter((tx) => tx.type === "debit" && tx.is_recurring) .map((tx) => ({ description: tx.narration, amount: tx.amount, frequency: "monthly" as const, category: (tx.category === "loan_emi" ? "emi" : "other") as | "emi" | "insurance" | "subscription" | "other", }));

    const salaryCredits = ( allTransactions as Array<{ type: string; category: string; amount: number; date: string; narration: string; }> ) .filter((tx) => tx.type === "credit" && tx.category === "salary") .map((tx) => ({ amount: tx.amount, date: tx.date, narration: tx.narration, }));

    return { averageMonthlyBalance: totalClosingBalance / paths.length, recurringDebits, salaryCredits, }; }

    Step 3: Calculate FOIR and Loan Eligibility

    With clean data in hand, run the FOIR calculation. The standard formula is:

    FOIR = (Total existing EMIs) / (Gross monthly income) × 100
    

    Most Indian lenders cap FOIR at 40–50% for salaried employees. The eligible new EMI is the headroom between the current FOIR and the cap.

    interface PreQualResult {
      applicantName: string;
      grossMonthlyIncome: number;
      netMonthlyIncome: number;
      existingEmiTotal: number;
      foirPercent: number;
      foirStatus: "pass" | "fail" | "borderline";
      eligibleNewEmi: number;
      estimatedLoanAmount: number; // at 8.5% for 20 years
      averageMonthlyBalance: number;
      verdict: "pre_qualified" | "refer_to_underwriter" | "declined";
      notes: string[];
    }
    

    function calculateEligibility( salaryData: SalaryData[], bankData: BankData, ): PreQualResult { const FOIR_CAP = 0.5; // 50% cap const LOAN_RATE = 0.085 / 12; // 8.5% annual, monthly const LOAN_TENURE_MONTHS = 240; // 20 years

    // Average gross and net over the 3 pay slips const avgGross = salaryData.reduce((s, m) => s + m.grossSalary, 0) / salaryData.length; const avgNet = salaryData.reduce((s, m) => s + m.netSalary, 0) / salaryData.length;

    // Only count EMI-category debits in FOIR numerator const existingEmiTotal = bankData.recurringDebits .filter((d) => d.category === "emi") .reduce((s, d) => s + d.amount, 0);

    const foirPercent = (existingEmiTotal / avgGross) * 100;

    const eligibleNewEmi = avgGross * FOIR_CAP - existingEmiTotal;

    // EMI formula: P = EMI × [(1+r)^n - 1] / [r × (1+r)^n] const estimatedLoanAmount = eligibleNewEmi > 0 ? (eligibleNewEmi * (Math.pow(1 + LOAN_RATE, LOAN_TENURE_MONTHS) - 1)) / (LOAN_RATE * Math.pow(1 + LOAN_RATE, LOAN_TENURE_MONTHS)) : 0;

    const notes: string[] = [];

    let foirStatus: "pass" | "fail" | "borderline"; if (foirPercent <= 40) foirStatus = "pass"; else if (foirPercent <= 50) foirStatus = "borderline"; else foirStatus = "fail";

    if (bankData.averageMonthlyBalance < avgNet * 0.5) { notes.push("Low average monthly balance relative to income"); }

    if (bankData.salaryCredits.length < 3) { notes.push( "Fewer than 3 salary credits found — manual verification recommended", ); }

    const verdict: PreQualResult["verdict"] = foirStatus === "pass" && notes.length === 0 ? "pre_qualified" : foirStatus === "fail" ? "declined" : "refer_to_underwriter";

    return { applicantName: salaryData[0].employerName, // placeholder grossMonthlyIncome: Math.round(avgGross), netMonthlyIncome: Math.round(avgNet), existingEmiTotal: Math.round(existingEmiTotal), foirPercent: Math.round(foirPercent * 10) / 10, foirStatus, eligibleNewEmi: Math.round(Math.max(eligibleNewEmi, 0)), estimatedLoanAmount: Math.round(Math.max(estimatedLoanAmount, 0)), averageMonthlyBalance: Math.round(bankData.averageMonthlyBalance), verdict, notes, }; }

    Step 4: Wire It Into an API Route

    Wrap the agent in an HTTP handler so your loan origination system can call it:

    import { Hono } from "hono";
    

    const app = new Hono();

    app.post("/api/pre-qualify", async (c) => { const body = await c.req.parseBody();

    const salaryFiles = [ body["salary_1"], body["salary_2"], body["salary_3"], ].filter(Boolean) as File[];

    const bankFiles = [ body["bank_1"], body["bank_2"], body["bank_3"], body["bank_4"], body["bank_5"], body["bank_6"], ].filter(Boolean) as File[];

    if (salaryFiles.length < 2 || bankFiles.length < 3) { return c.json( { success: false, error: { code: "INSUFFICIENT_DOCUMENTS", message: "Minimum 2 salary slips and 3 bank statements required", }, }, 400, ); }

    const [salaryData, bankData] = await Promise.all([ extractSalarySlips(salaryFiles as unknown as string[]), extractBankStatements(bankFiles as unknown as string[]), ]);

    const result = calculateEligibility(salaryData, bankData);

    return c.json({ success: true, data: result }); });

    Sample Output

    A successful pre-qualification returns structured JSON your underwriting system can act on directly:

    {
      "success": true,
      "data": {
        "grossMonthlyIncome": 95000,
        "netMonthlyIncome": 78500,
        "existingEmiTotal": 18000,
        "foirPercent": 18.9,
        "foirStatus": "pass",
        "eligibleNewEmi": 29500,
        "estimatedLoanAmount": 3097420,
        "averageMonthlyBalance": 124000,
        "verdict": "pre_qualified",
        "notes": []
      }
    }
    

    For a declined case:

    {
      "success": true,
      "data": {
        "grossMonthlyIncome": 62000,
        "netMonthlyIncome": 49000,
        "existingEmiTotal": 33000,
        "foirPercent": 53.2,
        "foirStatus": "fail",
        "eligibleNewEmi": 0,
        "estimatedLoanAmount": 0,
        "verdict": "declined",
        "notes": ["FOIR exceeds 50% cap"]
      }
    }
    

    Handling Edge Cases

    Joint applicants: Run extractSalarySlips separately for each applicant and sum the gross incomes before calculating FOIR. Most lenders allow combining income for co-applicants. Variable pay / commission-based roles: Lekha returns variable_pay as a separate field. Apply a haircut (typically 50% per RBI guidelines) before adding it to gross income. Self-employed applicants: Use ITR extraction (type: "itr") instead of salary slips. The net income is taken from Schedule BP or the computation of total income. Multiple bank accounts: Pass all statements from all accounts — the agent aggregates them automatically and deduplicates recurring debits.

    Why Not Do This with Raw OCR?

    Indian salary slips vary dramatically — a Zepto engineer's Darwinbox slip looks nothing like a government employee's PFMS slip or a garment factory worker's handwritten slip. Raw OCR returns text blobs you'd need custom parsing logic per employer format.

    Lekha's vision-AI pipeline handles all formats uniformly and returns a consistent schema, so your agent logic never has to know which payroll software the employer uses. See the full breakdown in our docs.

    What to Build Next

  • Add ITR verification to cross-check declared income
  • Connect to a credit bureau API to fetch existing loan obligations and reconcile with the bank statement debits
  • Integrate with your LOS (Loan Origination System) to trigger a full underwriting workflow on pre_qualified verdicts
  • Try the full flow in the Lekha playground — upload a sample payslip and bank statement to see the extracted JSON instantly

  • FAQ

    What is FOIR in home loans? FOIR (Fixed Obligation to Income Ratio) is the percentage of gross monthly income already committed to existing loan EMIs. Indian lenders typically require FOIR ≤ 50% before considering a new home loan application. How many months of bank statements are needed for home loan pre-qualification? Most Indian lenders require 6 months of bank statements. Some lenders accept 3 months for salaried applicants with strong CIBIL scores. The Lekha agent parses statements from all major Indian banks in the same format. Can Lekha parse salary slips from any company? Yes. Lekha uses vision AI rather than template-based OCR, so it handles payslips from Darwinbox, Keka, Greytip, SAP, custom PDFs, and even scanned printed slips — all returning the same structured JSON schema. How accurate is automated FOIR calculation? The calculation is only as accurate as the document extraction. Lekha achieves 97%+ field-level accuracy on structured payslips and major bank statements. For borderline cases (FOIR between 40–55%), always route to a human underwriter for review.

    Ready to build your lending agent? Start with the Lekha API docs or sign up free — no credit card required.