A single REST API to upload documents, run AI extraction, and retrieve structured JSON, fields, line items and confidence scores, ready to stream into any application or data pipeline.
Every request to the LenderAnalyzer API must carry a valid API key issued from your dashboard. Keys are scoped to your workspace and carry the permissions of the account that created them. Treat them as passwords, never commit them to source control.
Authorization: Bearer YOUR_API_KEY
The first step in any extraction workflow is uploading the source file. LenderAnalyzer accepts PDF, PNG, JPG, TIFF and WEBP formats, including multi-page PDFs and high-resolution scans. The endpoint returns a file_id that you pass to the Extract endpoint.
POST
/api/documents/upload
| Field | Type | Required | Description |
|---|---|---|---|
| document | file | required | PDF, PNG, JPG, TIFF or WEBP. Max 50 MB. |
POST /api/documents/upload → 200
{
"success": true,
"message": "File uploaded successfully",
"data": {
"file_id": 1234,
"name": "invoice-q1-2026.pdf"
}
}
Kick off an AI extraction job for a previously uploaded file. LenderAnalyzer queues the job asynchronously and returns an extraction_hash immediately. Poll GET /api/documents/extraction/{hash} for the result, or register a webhook endpoint and receive it as soon as the job finishes, see the Webhooks section.
POST
/api/documents/extract
| Field | Type | Required | Description |
|---|---|---|---|
| file_id | integer | required | The file_id returned by the upload endpoint. |
| document_type_id | integer | optional | ID of an extraction template from GET /api/documents/types. Omit it for bank statements: the built-in Bank Statement type is used, the same as in the web app. |
POST /api/documents/extract
{
"file_id": 1234,
"document_type_id": 5678
}
→ 202 Accepted
{
"success": true,
"message": "Extraction initialized",
"data": {
"extraction_id": 9876,
"extraction_hash": "abc123-def456"
}
}
Fetch the structured output for a completed extraction job. The response contains every extracted field with its value and page number, plus fully-parsed line-item tables where present. Each field carries a confidence score between 0 and 1 so downstream systems can route uncertain fields to human review.
GET
/api/documents/extraction/{hash}
| Param | Description |
|---|---|
| hash | The extraction_hash returned by POST /extract. |
GET /api/documents/extraction/abc123-def456 → 200
{
"success": true,
"data": {
"extraction_id": 9876,
"extraction_hash": "abc123-def456-ghi789-jkl012",
"status": "completed",
"confidence": 0.987,
"file": {
"id": 1234,
"name": "invoice-q1-2026.pdf"
},
"document_type": {
"id": 5678,
"name": "Invoice"
},
"fields": [
{
"field_id": 101,
"content": "vendor",
"value": "Acme Supplies Ltd",
"confidence": 0.998,
"is_table": false,
"page": 1
},
{
"field_id": 102,
"content": "document_number",
"value": "INV-2026-0892",
"confidence": 0.994,
"is_table": false,
"page": 1
},
{
"field_id": 103,
"content": "issue_date",
"value": "2026-03-14",
"confidence": 0.991,
"is_table": false,
"page": 1
},
{
"field_id": 104,
"content": "currency",
"value": "USD",
"confidence": 1.0,
"is_table": false,
"page": 1
},
{
"field_id": 105,
"content": "total",
"value": "594.00",
"confidence": 0.987,
"is_table": false,
"page": 1
},
{
"field_id": 106,
"content": "line_items",
"is_table": true,
"page": 1,
"rows": [
{
"cells": [
{ "value": "Steel brackets", "is_header": false },
{ "value": "120", "is_header": false },
{ "value": "4.50", "is_header": false },
{ "value": "540.00", "is_header": false }
]
}
]
}
]
}
}
| Status | Meaning |
|---|---|
| pending | The job is queued and waiting for a worker. |
| processing | The AI model is reading and extracting fields. |
| completed | Extraction finished successfully. fields are populated. |
| failed | An unrecoverable error occurred. Retry or contact support. |
GET
/api/documents/income?hashes[]={hash}&account_type=business&months=12
Pass the extraction hashes of 12 or 24 months of statements. The response has the eligible deposits, every excluded deposit with its reason (transfers between the borrower's own accounts, loan proceeds, tax refunds, refunds and reversals), the ownership and expense factor applied, the full-period and recent-period monthly income and the lower of the two as qualifying_monthly_income. Optional parameters: ownership_pct (1 to 100), expense_factor_pct (0 to 90, default 50 on business and 0 on personal) and large_deposit_threshold. large_deposits lists every deposit at or above the threshold, largest first, each with sourcing_tag (unexplained, own_transfer, loan_proceeds, gift, asset_sale and others), sourcing_label and the condition it usually needs. Every deposit carries its source file and page. The numbers match the income calculation screen in the app.
Return every extraction template available to your workspace, LenderAnalyzer built-in types and any custom templates your team has configured. Use the returned id as document_type_id in the Extract endpoint to target a specific field schema.
GET
/api/documents/types
GET /api/documents/types → 200
{
"success": true,
"data": [
{
"id": 5678,
"name": "Invoice",
"description": "Vendor invoices and bills, all layouts",
"is_public": true,
"fields_count": 12
},
{
"id": 5679,
"name": "Purchase Order",
"description": "Purchase orders with line-item tables",
"is_public": false,
"fields_count": 9
},
{
"id": 5680,
"name": "Bank Statement",
"description": "Transactions, balances and account details",
"is_public": true,
"fields_count": 8
}
]
}
Register an HTTPS endpoint in your dashboard and LenderAnalyzer posts every finished extraction to it, so you do not have to poll
GET /api/documents/extraction/{hash}
in a loop. Each request carries an HMAC signature you verify with your signing secret. Webhooks are part of REST API access on the Pro plan.
POST https://your-app.com/webhook/lenderanalyzer
{
"event": "extraction.completed",
"extraction_hash": "abc123-def456-ghi789-jkl012",
"extraction_id": 9876,
"status": "completed",
"document_type": "Bank Statement",
"file": { "id": 4410, "name": "march-statement.pdf" },
"fields": [
{ "field_id": 5460, "content": "Account holder", "value": "Acme Trading LLC", "is_table": 0, "page": 1 },
{ "field_id": 5461, "content": null, "is_table": 1, "page": 1,
"table": [
{ "row_index": 0, "is_header": true, "cells": [ { "content": "Date" }, { "content": "Description" }, { "content": "Balance" } ] },
{ "row_index": 1, "is_header": false, "cells": [ { "content": "03/04" }, { "content": "ACH DEPOSIT" }, { "content": "12,480.15" } ] }
] }
]
}
The fields array is byte for byte the same shape you get from GET /api/documents/extraction/{hash}, so one parser handles both.
LenderAnalyzer uses standard HTTP status codes. Every error response includes a machine-readable error_code string and a human-readable message to simplify debugging.
| HTTP | error_code | When it happens |
|---|---|---|
| 400 | invalid_request | Missing or malformed required field in the request body. |
| 401 | unauthorized | No API key supplied, or the key is invalid or revoked. |
| 403 | forbidden | Your plan does not include API access, or you hit a quota. |
| 404 | not_found | The file_id or extraction_hash does not exist in your workspace. |
| 415 | unsupported_media | The uploaded file format is not accepted (use PDF, PNG, JPG, TIFF, WEBP). |
| 422 | extraction_failed | The AI could not extract meaningful data from the document. |
| 429 | rate_limit_exceeded | Too many requests. Check Retry-After header for the cooldown window. |
| 500 | server_error | An internal error occurred. Retry with exponential back-off. |
→ 401 Unauthorized
{
"success": false,
"error_code": "unauthorized",
"message": "Invalid or missing API key."
}
Rate limits are applied per API key on a rolling 60-second window. If you exceed the limit the API returns HTTP 429 with a Retry-After header indicating the number of seconds to wait before retrying.
| Plan | Requests / hour | Max file size | Concurrent jobs |
|---|---|---|---|
| Pro | 500 | 50 MB | 10 |
Response headers on every call
X-RateLimit-Limit: 500 X-RateLimit-Remaining: 487 X-RateLimit-Reset: 1748987200 Retry-After: 42 only present on 429
Need higher throughput? Enterprise covers 350,000 pages a month and is bought directly in checkout: see Enterprise pricing.
Copy-paste snippets that cover the full extraction lifecycle, upload a document, trigger extraction, and retrieve the structured JSON result. Every example uses the same field names and extraction hash so you can follow the flow across languages.
shell, full extraction workflow
# 1, Upload the document curl -X POST https://lenderanalyzer.com/api/documents/upload \ -H "Authorization: Bearer YOUR_API_KEY" \ -F "document=@/path/to/invoice-q1-2026.pdf" # → {"success":true,"data":{"file_id":1234,"name":"invoice-q1-2026.pdf"}} # 2, Start extraction (document_type_id optional) curl -X POST https://lenderanalyzer.com/api/documents/extract \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{"file_id": 1234, "document_type_id": 5678}' # → {"success":true,"data":{"extraction_id":9876,"extraction_hash":"abc123-def456"}} # 3, Retrieve structured results curl -X GET https://lenderanalyzer.com/api/documents/extraction/abc123-def456 \ -H "Authorization: Bearer YOUR_API_KEY"
extract.php
<?php $apiKey = 'YOUR_API_KEY'; $baseUrl = 'https://lenderanalyzer.com/api'; $filePath = '/path/to/invoice-q1-2026.pdf'; // 1, Upload $ch = curl_init("$baseUrl/documents/upload"); curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_POST => true, CURLOPT_HTTPHEADER => ["Authorization: Bearer $apiKey"], CURLOPT_POSTFIELDS => ['document' => new CURLFile($filePath)], ]); $upload = json_decode(curl_exec($ch), true); curl_close($ch); if (!$upload['success']) { die("Upload failed: " . $upload['message']); } $fileId = $upload['data']['file_id']; // 2, Extract $ch = curl_init("$baseUrl/documents/extract"); curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_POST => true, CURLOPT_HTTPHEADER => [ "Authorization: Bearer $apiKey", "Content-Type: application/json", ], CURLOPT_POSTFIELDS => json_encode([ 'file_id' => $fileId, 'document_type_id' => 5678, optional ]), ]); $extract = json_decode(curl_exec($ch), true); curl_close($ch); if (!$extract['success']) { die("Extraction failed: " . $extract['message']); } $hash = $extract['data']['extraction_hash']; // 3, Poll for results do { sleep(2); $ch = curl_init("$baseUrl/documents/extraction/$hash"); curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => ["Authorization: Bearer $apiKey"], ]); $result = json_decode(curl_exec($ch), true); curl_close($ch); } while ($result['data']['status'] === 'processing'); foreach ($result['data']['fields'] as $field) { echo $field['content'] . ': ' . $field['value'] . "\n"; }
extract.mjs
const API_KEY = 'YOUR_API_KEY'; const BASE_URL = 'https://lenderanalyzer.com/api'; async function uploadDocument(file) { const form = new FormData(); form.append('document', file); const res = await fetch(`${BASE_URL}/documents/upload`, { method: 'POST', headers: { 'Authorization': `Bearer ${API_KEY}` }, body: form, }); const json = await res.json(); if (!json.success) throw new Error(json.message); return json.data.file_id; } async function startExtraction(fileId, documentTypeId = null) { const payload = { file_id: fileId }; if (documentTypeId) payload.document_type_id = documentTypeId; const res = await fetch(`${BASE_URL}/documents/extract`, { method: 'POST', headers: { 'Authorization': `Bearer ${API_KEY}`, 'Content-Type': 'application/json', }, body: JSON.stringify(payload), }); const json = await res.json(); if (!json.success) throw new Error(json.message); return json.data.extraction_hash; } async function pollResults(hash, intervalMs = 2000) { while (true) { const res = await fetch(`${BASE_URL}/documents/extraction/${hash}`, { headers: { 'Authorization': `Bearer ${API_KEY}` }, }); const json = await res.json(); if (json.data.status === 'completed') return json.data; if (json.data.status === 'failed') throw new Error('Extraction failed'); await new Promise(r => setTimeout(r, intervalMs)); } } Usage const fileInput = document.querySelector('input[type="file"]'); const fileId = await uploadDocument(fileInput.files[0]); const hash = await startExtraction(fileId, 5678); const data = await pollResults(hash); console.log('Extracted fields:', data.fields);
extract.py
import time import requests API_KEY = "YOUR_API_KEY" BASE_URL = "https://lenderanalyzer.com/api" HEADERS = {"Authorization": f"Bearer {API_KEY}"} def upload_document(file_path: str) -> int: with open(file_path, "rb") as f: resp = requests.post( f"{BASE_URL}/documents/upload", headers=HEADERS, files={"document": f}, ) data = resp.json() if not data["success"]: raise RuntimeError(data["message"]) return data["data"]["file_id"] def start_extraction(file_id: int, document_type_id: int | None = None) -> str: payload = {"file_id": file_id} if document_type_id: payload["document_type_id"] = document_type_id resp = requests.post( f"{BASE_URL}/documents/extract", headers={**HEADERS, "Content-Type": "application/json"}, json=payload, ) data = resp.json() if not data["success"]: raise RuntimeError(data["message"]) return data["data"]["extraction_hash"] def poll_results(extraction_hash: str, interval: float = 2.0) -> dict: url = f"{BASE_URL}/documents/extraction/{extraction_hash}" while True: data = requests.get(url, headers=HEADERS).json()["data"] if data["status"] == "completed": return data if data["status"] == "failed": raise RuntimeError("Extraction failed") time.sleep(interval) # Usage file_id = upload_document("/path/to/invoice-q1-2026.pdf") ex_hash = start_extraction(file_id, document_type_id=5678) result = poll_results(ex_hash) for field in result["fields"]: print(f"{field['content']}: {field['value']} (confidence={field['confidence']})")
Generate your first API key and start pulling structured data from your documents in minutes.
New here? See what the bank statement analysis API returns for lenders: transactions plus computed income, average daily balance, NSF counts and existing debt service.