Vercel AI SDK: Parse Indian Financial Docs in Next.js
Step-by-step guide to integrating Lekha with the Vercel AI SDK. Extract bank statements, salary slips, and CAS reports as structured JSON in your Next.js app.
The Vercel AI SDK has become the go-to toolkit for shipping AI-powered Next.js apps. It handles streaming, tool calls, and multi-step agent loops with very little boilerplate. What it doesn't include out of the box is the ability to understand Indian financial documents — bank statements, CAS reports, salary slips, ITRs.
That's where Lekha comes in. In this guide you'll wire Lekha into the Vercel AI SDK as a tool, so your AI agent can read any financial document a user uploads and return structured, typed JSON — ready for your application logic.
What You'll Build
A Next.js 14 API route that:
parseFinancialDocument tool to the AI modelBy the end you'll have a reusable pattern you can extend to loan eligibility checks, expense categorisation, net-worth dashboards, and more.
Prerequisites
bun or npmStep 1: Install Dependencies
bun add ai @ai-sdk/anthropic
Lekha is a plain REST API, so no extra SDK is needed.
Step 2: Create the Lekha Tool
The Vercel AI SDK's tool() helper lets you describe a function to the model and handle its execution. Create a dedicated file so you can reuse the tool across routes.
// lib/tools/parse-financial-document.ts
import { tool } from "ai";
import { z } from "zod";
const LEKHA_BASE = "https://api.lekhadev.com/v1";
export const parseFinancialDocument = tool({
description:
"Parse an Indian financial document (bank statement, salary slip, CAS, ITR) " +
"from a base64-encoded PDF and return structured JSON data.",
parameters: z.object({
fileBase64: z
.string()
.describe("Base64-encoded PDF content of the financial document"),
documentType: z
.enum([
"bank_statement",
"salary_slip",
"cas",
"itr",
"form16",
"gst",
"auto",
])
.default("auto")
.describe(
"Type of financial document. Use 'auto' to let Lekha detect it.",
),
}),
execute: async ({ fileBase64, documentType }) => {
const res = await fetch(${LEKHA_BASE}/extract, {
method: "POST",
headers: {
Authorization: Bearer ${process.env.LEKHA_API_KEY},
"Content-Type": "application/json",
},
body: JSON.stringify({
file: fileBase64,
document_type: documentType,
}),
});
if (!res.ok) {
const err = await res.json();
throw new Error(Lekha error: ${err.error?.message ?? res.statusText});
}
const { data } = await res.json();
return data; // typed JSON: transactions[], balances, account_holder, etc.
},
});
The execute function runs server-side whenever the model decides to call the tool. The model never sees your API key.
Step 3: Wire Up the API Route
Create a streaming API route that uses the tool.
// app/api/analyse-document/route.ts
import { streamText } from "ai";
import { anthropic } from "@ai-sdk/anthropic";
import { parseFinancialDocument } from "@/lib/tools/parse-financial-document";
export const maxDuration = 60; // allow up to 60s for large PDFs
export async function POST(req: Request) {
const { fileBase64, userMessage } = await req.json();
const result = await streamText({
model: anthropic("claude-sonnet-4-6"),
system:
"You are a financial document analyst. When the user uploads a document, " +
"call parseFinancialDocument to extract its data, then answer the user's question " +
"using the extracted information. Always cite specific figures from the document.",
messages: [
{
role: "user",
content: [
{
type: "text",
text: userMessage ?? "Please analyse this financial document.",
},
// Pass the file reference in the message so the model knows to call the tool
{
type: "text",
text: Document (base64 truncated for display): ${fileBase64.slice(0, 50)}...,
},
],
},
],
tools: { parseFinancialDocument },
// Allow the model to call the tool and then respond in one round-trip
maxSteps: 3,
// Pass the full base64 through tool_choice context
toolCallStreaming: true,
});
return result.toDataStreamResponse();
}
> Tip: maxSteps: 3 lets the model call parseFinancialDocument, receive the result, and then compose a response — all within a single streamText call. No manual loop needed.
Step 4: Build the Upload Component
On the client side, read the file as base64 and send it to your route.
// app/components/DocumentAnalyser.tsx
"use client";
import { useChat } from "ai/react";
import { useState } from "react";
export function DocumentAnalyser() {
const [fileBase64, setFileBase64] = useState(null);
const { messages, input, handleInputChange, handleSubmit, isLoading } =
useChat({ api: "/api/analyse-document" });
const onFileChange = (e: React.ChangeEvent) => {
const file = e.target.files?.[0];
if (!file) return;
const reader = new FileReader();
reader.onload = () => {
// Strip the data URL prefix — Lekha wants raw base64
const base64 = (reader.result as string).split(",")[1];
setFileBase64(base64);
};
reader.readAsDataURL(file);
};
const onSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (!fileBase64) return;
handleSubmit(e, {
body: { fileBase64, userMessage: input },
});
};
return (
Financial Document Analyser
{messages.map((m) => (
p-4 rounded ${m.role === "user" ? "bg-gray-100" : "bg-blue-50"}}
>
{m.role === "user" ? "You" : "Assistant"}
{m.content}
))}
);
}
Step 5: Set Environment Variables
# .env.local
LEKHA_API_KEY=lk_live_your_key_here
ANTHROPIC_API_KEY=sk-ant-your_key_here
Add LEKHA_API_KEY to your Vercel project environment variables in the dashboard before deploying.
What Lekha Returns
When parseFinancialDocument runs against a bank statement, data looks like this:
{
"document_type": "bank_statement",
"bank": "HDFC Bank",
"account_holder": "Priya Sharma",
"account_number": "XXXX1234",
"period": { "from": "2026-01-01", "to": "2026-03-31" },
"opening_balance": 42500.0,
"closing_balance": 87300.5,
"transactions": [
{
"date": "2026-01-04",
"description": "UPI/Swiggy/Food",
"debit": 450.0,
"credit": null,
"balance": 42050.0,
"category": "food"
}
// ... hundreds more
],
"summary": {
"total_credits": 320000.0,
"total_debits": 275199.5,
"average_monthly_balance": 61200.0
}
}
All amounts are numbers (never strings), all dates are ISO 8601, and transactions are auto-categorised. The model can reason over this structured data directly.
Extending the Pattern
Once the tool is wired up, you can do much more without changing your route:
| User question | What happens |
| ---------------------------------------- | -------------------------------------------------------------------------------------- |
| "What's my average monthly salary?" | Model calls tool, sums credit transactions labelled salary, divides by months |
| "Am I eligible for a ₹5L loan?" | Model checks average monthly balance and salary credits against your eligibility rules |
| "Summarise my top 5 spending categories" | Model aggregates transactions by category field |
| "Flag any unusual large debits" | Model scans for debits > 2× the average transaction |
The model handles the reasoning; Lekha handles the extraction. Your application logic stays clean.
Document Types Supported
Lekha handles the full range of Indian financial documents:
Pass document_type: "auto" and Lekha's classifier will detect the format automatically — useful when users can upload any document.
Performance Tips
Cache extraction results. A PDF extraction takes 2-5 seconds. If your user may ask multiple questions about the same document in a session, cache thedata object in memory or Redis and pass it directly in the system prompt for follow-up questions rather than calling Lekha again.
Stream tool results. toolCallStreaming: true in streamText pushes partial tool results to the client as they arrive, so the UI doesn't feel frozen while the PDF is being processed.
Handle large PDFs gracefully. Multi-page bank statements for 12-month periods can be 50+ pages. Lekha handles these natively, but set maxDuration = 60 in your route config (shown above) so Vercel doesn't time out the function.
FAQ
Does Lekha store my users' documents? No. Lekha processes documents in memory and returns the extracted JSON. No PDF or image is stored on Lekha's servers. This makes it compatible with India's DPDP Act requirements for data minimisation. Which Vercel AI SDK version does this work with? This guide targetsai v4+ (the version with streamText, tool(), and useChat from ai/react). If you're on v3, the streamText API is the same; only the import paths differ slightly.
Can I use this with OpenAI instead of Anthropic?
Yes — swap anthropic("claude-sonnet-4-6") for openai("gpt-4o") from @ai-sdk/openai. The tool() definition and execute function are provider-agnostic.
What happens if Lekha can't parse the document?
The API returns a structured error ({ success: false, error: { code, message } }). The execute function throws, which the Vercel AI SDK catches and surfaces as a tool error in the message stream. You can handle it in the onError callback of useChat.
The complete pattern — upload → tool call → extract → stream response — takes about 30 minutes to wire up. Once it's running, your Next.js app can answer natural-language questions about any Indian financial document your users upload.
Ready to start? Get your free Lekha API key at lekhadev.com and try extraction live in the playground — no code needed to see what the API returns for your own documents. Full API reference is at lekhadev.com/docs.