From dashboard to API: automating extraction
Prototype an extraction by hand, lock down the document type that gets it right, then wire it into your backend. The full path from a single upload to extraction at scale.
Priya Raghavan
Co-founder & CTO
5 min read
Most teams that automate with dOCR start the same way: someone drags a single invoice into the dashboard to see what comes back. That instinct is correct. The dashboard is where you figure out what to extract; the API is where you run it ten thousand times without thinking about it. The mistake is skipping the first half and writing integration code against a document type you haven’t actually pinned down yet.
This post walks the path end to end — prototype by hand, inspect the fields, lock the document type, then call the API from your backend with error handling that survives a real document feed.
Prototype in the dashboard first
Open the dashboard and upload one representative document. Not the cleanest scan you own — a typical one, with the smudge and the stapled second page. dOCR runs AI vision plus an LLM over it and returns structured JSON: the typed fields you defined, not a wall of raw OCR text. (If you’re still deciding what “structured” buys you over plain text, we made that case separately.)
What you’re doing here is cheap experimentation. Change the document type, re-upload, compare. The feedback loop is seconds, and you’re paying no integration cost to learn what the model can and can’t reliably pull from your paperwork.
Inspect the fields in Extraction History
Every dashboard upload lands in Extraction History with the full JSON response attached. This is the part people skip, and it’s the most valuable.
Open the result and read the actual shape:
{
"data": {
"invoiceNumber": "INV-20294",
"issueDate": "2026-02-11",
"vendorName": "Northwind Supplies",
"totalAmount": 1284.50,
"currency": "USD"
}
}
Run three or four representative documents through and compare their outputs side by side. You’re looking for the fields that come back populated and correctly typed every time versus the ones that wobble — a date that’s sometimes a string and sometimes null, a total that occasionally captures the subtotal instead. That wobble is your signal to refine the document type before you write a line of code, not after.
Lock down the document type
A document type in dOCR is a field schema you define — the contract for what every extraction returns. Tighten it based on what Extraction History showed you: name fields precisely, give the model the descriptions that disambiguate “total” from “amount due,” drop fields that never populate reliably. (The full reasoning behind custom document types is here.)
Treat this like a database schema, because downstream it effectively is one. Once your backend parses data.totalAmount as a number, a document type that starts returning a string is a production incident. Lock it before you automate, version it deliberately after.
Get an API key
When the dashboard prototype is solid, move to code. Generate an API key from your dashboard settings and store it as an environment variable — never in source:
export DOCR_API_KEY="sk_live_your_key_here"
Every API request authenticates with a Bearer token in the Authorization header. Treat the key like a password: server-side only, rotated on a schedule, scoped per environment so a leaked staging key can’t touch production.
Call the API
The endpoint is POST https://docr.dev/api/v1/extract. It takes a multipart form with two fields: file (the document) and documentType (the schema you locked down). Here it is with curl:
curl -X POST https://docr.dev/api/v1/extract \
-H "Authorization: Bearer $DOCR_API_KEY" \
-F "file=@invoice.pdf" \
-F "documentType=invoice"
And the same call from a Node backend with fetch:
import { readFile } from 'node:fs/promises';
async function extract(path: string, documentType: string) {
const form = new FormData();
const bytes = await readFile(path);
form.set('file', new Blob([bytes]), 'document.pdf');
form.set('documentType', documentType);
const res = await fetch('https://docr.dev/api/v1/extract', {
method: 'POST',
headers: { Authorization: `Bearer ${process.env.DOCR_API_KEY}` },
body: form,
});
if (!res.ok) {
const detail = await res.text();
throw new Error(`dOCR ${res.status}: ${detail}`);
}
const { data } = await res.json();
return data;
}
The response is the same { "data": { ...fields } } shape you already verified in Extraction History. That’s the payoff of prototyping first: the JSON your code parses is the JSON you already read with your own eyes.
Handle responses and errors
A real document feed will hand you files the dashboard never showed you — a password-protected PDF, a 14MB scan, a JPEG that’s actually a renamed .heic. Plan for it.
Supported formats are PDF, JPG/JPEG, PNG, BMP, WebP, and DOCX, up to 10MB and 15 pages. Validate against those limits before you spend an API call:
const MAX_BYTES = 10 * 1024 * 1024;
const ALLOWED = ['pdf', 'jpg', 'jpeg', 'png', 'bmp', 'webp', 'docx'];
function check(name: string, size: number) {
const ext = name.split('.').pop()?.toLowerCase() ?? '';
if (!ALLOWED.includes(ext)) throw new Error(`Unsupported format: ${ext}`);
if (size > MAX_BYTES) throw new Error(`File too large: ${size} bytes`);
}
For the call itself, branch on the status code. A 4xx is your problem to fix — bad key, malformed request, a file that slipped past validation — and retrying won’t help. A 5xx or a network blip is worth one or two retries with backoff. Don’t retry blindly on every failure; you’ll just turn one bad document into a hundred wasted calls.
Batch at scale, within the limits
Once a single call is solid, scale it. The limits to respect are per file — 10MB, 15 pages — so for documents longer than 15 pages you’ll split before you send.
For throughput, process a feed with bounded concurrency rather than firing everything at once. A few requests in flight keeps things fast without hammering the endpoint:
async function extractAll(files: string[], documentType: string) {
const LIMIT = 5;
const results = [];
for (let i = 0; i < files.length; i += LIMIT) {
const batch = files.slice(i, i + LIMIT);
const settled = await Promise.allSettled(
batch.map((f) => extract(f, documentType)),
);
results.push(...settled);
}
return results;
}
Promise.allSettled matters here: one corrupt file in a batch of a thousand should land in a dead-letter queue for review, not abort the whole run. Log the failures with enough context to reprocess them later, and keep moving.
The shape of the whole thing
The pattern is the same whether you’re processing five documents a week or fifty thousand a day. Prove the extraction by hand, read the JSON until you trust it, freeze the document type, then let your backend do the boring part forever. The dashboard is where you think; the API is where you stop having to.