← Back to blog
·8 min read

OpenAI Agents SDK: Parse Indian Financial Documents

Integrate Lekha with the OpenAI Agents SDK to give your TypeScript agents structured access to Indian bank statements, salary slips, and CAS reports.

openai agents sdkfinancial document apiindian fintechtypescriptai agentbank statement parsertool usedocument extraction

The OpenAI Agents SDK (@openai/agents) is the official TypeScript framework for building production-ready AI agents with tool use, handoffs, and tracing baked in. It pairs naturally with Lekha: your agent handles reasoning and orchestration, Lekha handles the messy reality of Indian financial documents.

This guide shows you how to wire the two together — from a single tool call that extracts a bank statement to a multi-step agent that compares an applicant's income across three months and returns a lending decision.

Why Lekha + OpenAI Agents SDK?

The OpenAI Agents SDK solves tool orchestration, streaming, and multi-agent handoffs. What it doesn't know anything about is the maze of formats inside an HDFC NetBanking PDF, a Kotak salary slip, or a 68-page CAS statement. That's Lekha's job.

Together, the split of responsibility is clean:

  • OpenAI Agents SDK — decides _when_ to call a tool, chains calls, handles retries, streams output to the user
  • Lekha — turns a raw PDF or image into typed JSON a model can reason over
  • You never have to prompt-engineer around document layout quirks or write regex for SBI transaction descriptions again.

    Prerequisites

  • Node.js 20+ or Bun 1.x
  • A Lekha API key — grab one free at lekhadev.com
  • An OpenAI API key
  • bun add @openai/agents
    

    Lekha has no SDK dependency — you call it over HTTP

    Step 1: Define a Lekha Tool

    The Agents SDK lets you define tools with a JSON schema and an execute function. Wrap the Lekha /extract endpoint as a tool:

    // tools/lekha.ts
    import { tool } from "@openai/agents";
    import { z } from "zod";
    

    const LEKHA_API = "https://api.lekhadev.com/v1/extract"; const LEKHA_KEY = process.env.LEKHA_API_KEY!;

    export const extractFinancialDocument = tool({ name: "extract_financial_document", description: "Extract structured data from an Indian financial document (bank statement, salary slip, CAS report, ITR, Form 16). Returns typed JSON with transactions, balances, and metadata.", parameters: z.object({ document_url: z .string() .describe("Public URL of the PDF or image to extract"), document_type: z .enum([ "bank_statement", "salary_slip", "cas_statement", "itr", "form_16", "cibil_report", "gst_invoice", "balance_sheet", ]) .optional() .describe( "Hint the document type to improve accuracy. Omit to auto-detect.", ), }), execute: async ({ document_url, document_type }) => { const res = await fetch(LEKHA_API, { method: "POST", headers: { Authorization: Bearer ${LEKHA_KEY}, "Content-Type": "application/json", }, body: JSON.stringify({ url: document_url, document_type }), });

    if (!res.ok) { const err = await res.json(); throw new Error(Lekha error ${res.status}: ${err.error?.message}); }

    const { data } = await res.json(); return data; // Structured JSON — bank statement, salary slip, etc. }, });

    A few things worth noting:

  • document_type is optional — Lekha's classifier will auto-detect most formats, but passing a hint speeds up extraction on ambiguous documents
  • The tool throws on HTTP errors so the SDK can surface the failure cleanly in traces
  • The return value is whatever Lekha sends back: for a bank statement that's account metadata, an array of transactions with ISO 8601 dates and numeric amounts, and running balances
  • Step 2: Create the Agent

    With the tool defined, creating an agent that can understand Indian financial documents takes about ten lines:

    // agent.ts
    import { Agent, run } from "@openai/agents";
    import { extractFinancialDocument } from "./tools/lekha";
    

    export const financialAgent = new Agent({ name: "Indian Financial Document Analyst", model: "gpt-4o", instructions: You are a financial analyst specialising in Indian financial documents. When a user provides a document URL, use extract_financial_document to get structured data. Always report monetary amounts in INR. Use ISO dates (YYYY-MM-DD) in your responses. Summarise clearly: account holder, period, opening/closing balance, total credits, total debits, and any notable patterns., tools: [extractFinancialDocument], });

    Run it:

    // main.ts
    import { run } from "@openai/agents";
    import { financialAgent } from "./agent";
    

    const result = await run(financialAgent, { input: "Please analyse this HDFC bank statement: https://storage.example.com/statements/hdfc-jan-2026.pdf", });

    console.log(result.finalOutput);

    The SDK handles the agentic loop automatically: the model decides to call extract_financial_document, the tool hits Lekha, the structured JSON comes back, and the model synthesises a human-readable summary.

    Step 3: Multi-Document Income Verification

    A common lending use case is verifying three months of salary and bank statement data. The Agents SDK's parallel tool calls make this straightforward:

    // income-verification.ts
    import { Agent, run } from "@openai/agents";
    import { extractFinancialDocument } from "./tools/lekha";
    
      const incomeVerifier = new Agent({ name: "Income Verifier", model: "gpt-4o", instructions: You verify income for loan eligibility. Given multiple documents:
    • Extract each document using extract_financial_document
    • Cross-check salary slip net pay against bank credit entries for the same month
    • Calculate average monthly income over the period
    • Flag discrepancies greater than 10% between declared salary and bank credits
    • Return a structured verdict: { eligible: boolean, averageMonthlyIncome: number, discrepancies: string[] },
    • tools: [extractFinancialDocument], });

    const applicantDocs = { salarySlips: [ "https://storage.example.com/docs/salary-nov-2025.pdf", "https://storage.example.com/docs/salary-dec-2025.pdf", "https://storage.example.com/docs/salary-jan-2026.pdf", ], bankStatements: [ "https://storage.example.com/docs/sbi-nov-2025.pdf", "https://storage.example.com/docs/sbi-dec-2025.pdf", "https://storage.example.com/docs/sbi-jan-2026.pdf", ], };

    const prompt = Verify income eligibility for a loan applicant. Salary slips: ${applicantDocs.salarySlips.join(", ")} Bank statements: ${applicantDocs.bankStatements.join(", ")} Minimum required monthly income: ₹50,000 ;

    const result = await run(incomeVerifier, { input: prompt }); console.log(result.finalOutput);

    The model will issue parallel calls to extract_financial_document for all six documents, then cross-reference the numbers. What used to require a custom ETL pipeline is now a 40-line TypeScript file.

    Step 4: Streaming to the UI

    For user-facing applications, stream the agent's progress in real time:

    import { run } from "@openai/agents";
    import { financialAgent } from "./agent";
    

    const stream = run(financialAgent, { input: "Analyse this CAS statement: https://example.com/cas-2025.pdf", stream: true, });

    for await (const event of stream) { if (event.type === "agent_updated_stream_event") { process.stdout.write(event.data.delta ?? ""); } if (event.type === "tool_call_item") { console.log(\n[Tool called: ${event.rawItem.name}]\n); } }

    Users see the agent's reasoning appear as it works — including which tool calls it's making — without waiting for the full extraction to complete.

    Step 5: Handling Lekha's Typed Responses

    Different document types return different schemas. Here's how to type the Lekha response for a bank statement so TypeScript catches misuse at compile time:

    // types/lekha.ts
    export interface LekhaTransaction {
      date: string; // ISO 8601: "2026-01-15"
      description: string;
      credit: number | null;
      debit: number | null;
      balance: number;
      category?: string;
      reference?: string;
    }
    

    export interface LekhaBankStatement { document_type: "bank_statement"; bank_name: string; account_number: string; account_holder: string; ifsc_code?: string; period: { from: string; to: string }; opening_balance: number; closing_balance: number; total_credits: number; total_debits: number; transactions: LekhaTransaction[]; }

    Update the tool's execute to cast the return:

    execute: async ({ document_url, document_type }) => {
      // ... fetch call ...
      const { data } = await res.json();
      return data as LekhaBankStatement; // or union type for all doc types
    },
    

    What Lekha Handles So You Don't Have To

    When you extract an Indian bank statement with Lekha, you get clean data regardless of the source bank:

    | Problem | What Lekha does | | ----------------------------------------------- | ----------------------------------------------- | | Scanned PDFs with skewed text | Vision model extracts directly from the image | | Non-standard date formats (01-Jan-26, 01/01/26) | Normalised to ISO 8601 | | Amounts with commas (₹1,45,000) | Returned as numeric 145000 | | Debit/credit in a single column | Split into separate credit and debit fields | | Multi-page statements (50+ pages) | Full document processed in one API call | | 50+ bank formats (HDFC, SBI, ICICI, Axis…) | Single unified schema |

    Try it in your browser at lekhadev.com/playground before writing any code.

    Tracing and Observability

    The Agents SDK ships with built-in tracing. Every Lekha tool call appears in the trace with its inputs, outputs, and latency — useful for debugging extraction failures or slow responses:

    import { Agent, run, setTracingEnabled } from "@openai/agents";
    

    setTracingEnabled(true); // Traces appear in the OpenAI dashboard

    const result = await run(financialAgent, { input: "..." });

    For production, pair this with structured logging on the Lekha side — the API returns a request_id in every response you can correlate across systems.

    FAQ

    Does Lekha work with base64-encoded documents instead of URLs?

    Yes. Pass { base64: "", mime_type: "application/pdf" } in the request body instead of { url: "..." }. Update the tool schema's parameters to accept either shape and adjust the execute function accordingly. This is useful when your agent receives a document as a buffer rather than a remote URL.

    How many documents can I process in parallel?

    On a paid Lekha plan, there's no hard concurrency limit — the SDK will fire all tool calls simultaneously and your throughput is bounded by your rate limit tier. For high-volume batch jobs, see the batch processing guide.

    Can the agent handle password-protected PDFs?

    Pass the password in the Lekha request body: { url: "...", password: "yourpassword" }. Add a password field to the Zod schema and thread it through. Lekha decrypts the PDF server-side before extraction.

    What if the document type can't be classified?

    Lekha returns { success: false, error: { code: "UNSUPPORTED_FORMAT" } }. Your execute function should throw on this so the agent sees the failure in its tool output and can ask the user to provide a different file.

    Get Started

    The full source for this guide is available in the Lekha docs. If you're building a financial agent and want to skip the document parsing complexity entirely:

  • Sign up at lekhadev.com — free tier includes 50 extractions/month
  • Try the playground at lekhadev.com/playground to see extraction output before writing any code
  • Read the API reference at lekhadev.com/docs for the full schema for every document type
  • Indian financial documents are hard. Your agent doesn't have to be.