Tabscanner for AI agents
Give a language model the ability to read receipts. This page is written for the developer wiring the tool up and for the agent that ends up calling it.
Overview
Tabscanner turns a photo of a receipt into JSON: merchant, date, totals, taxes, tip, payment method and line items, each with a confidence score. The HTTP API is two calls, an upload that returns a token and a result endpoint that is polled until the data is ready. See the API reference for every field.
For an agent, hide the polling. Expose one tool, scan_receipt, that takes an image and returns the result object, plus a trivial get_credit. The model should never have to decide when to poll again, and it should never see the API key.
Everything the model needs to know about the data is in the field descriptions of openapi.yaml. Put the AGENTS.md snippet in your agent's instructions if it will also call the HTTP API directly.
Every Tabscanner response is HTTP 200. The outcome is the numeric code in the JSON body. 301 means "not ready yet", 202 means "done", 402 means "unknown token". A tool that checks response.ok will treat every failure as success.
Rules for agents
If you are an agent calling the HTTP API directly, follow these rules exactly.
- Send the API key in the
apikeyheader. Read it from the environment; never print or log it. - Upload with
POST https://api.tabscanner.com/api/2/processasmultipart/form-datawith exactly one file part namedfile(JPG or PNG). Addregionwith the receipt's two-letter country code when you know it. - Read
codefrom the JSON body.200or300means accepted: keep thetoken. Anything else is an error: stop and reportmessage. - Wait one second, then
GET https://api.tabscanner.com/api/result/{token}. Ifcodeis301, wait one second and call again. If it is202, the data is inresult. Any other code is an error: stop and reportmessage. - Stop polling after 60 seconds and report a timeout. The token stays valid, so the result can be collected later.
- Never retry a 4xx code with the same input. Retry 5xx codes with backoff, except
500(OCR failure), which means the image itself cannot be read. - Each accepted upload uses one credit. Do not upload the same image twice to "check"; identical bytes return
duplicate: trueand the original token. - Ignore result fields you do not recognise. New fields are added without a version change.
| Code | Meaning | What to do |
|---|---|---|
200 | Process request submitted successfully | Store the returned token and start polling the result endpoint after about 2 seconds. |
202 | Result available | Read the result object. Stop polling. |
300 | Image uploaded, but did not meet the recommended dimension of 720x1280 (WxH) | Processing continues. Accuracy may be reduced; capture higher resolution images if possible. |
301 | Result not yet available | Keep polling, about once per second. Give up after 60 seconds and report a timeout. |
400 | API key not found | Check the apikey header. Do not retry with the same key. |
401 | Not enough credit | Top up credit or upgrade the plan. Do not retry until credit is available. |
402 | Token not found | The token is unknown or older than the retention period. Do not retry. |
403 | No file detected | Send exactly one multipart part named file. |
404 | Multiple files detected, can only upload 1 file per API call | Send one file per call. |
405 | Unsupported mimetype | Send image/jpeg or image/png. |
406 | Form parser error | The multipart body is malformed. Check the request encoding. |
407 | Unsupported file extension | Use a .jpg, .jpeg or .png filename in the multipart part. |
408 | File system error | Retry once. If it persists contact support. |
500 | OCR Failure | The image could not be processed. Retry with a clearer image. |
510 | Server error | Retry with exponential backoff. |
520 | Database Connection Error | Retry with exponential backoff. |
521 | Database Query Error | Retry with exponential backoff. |
Tool definitions
The JSON on the right is ready for the Claude Messages API tools array. The input_schema properties other than image_path map one to one onto the form fields of the process endpoint, and the test suite checks that they stay a subset of the API's parameters.
OpenAI-style function calling
Wrap each definition as {"type": "function", "function": {"name", "description", "parameters"}}, using the same schema under parameters instead of input_schema. Nothing else changes.
Design notes
image_pathis a local path because the agent runtime usually has the file. If your runtime passes URLs or base64, adapt the implementation, not the schema the model sees.- Keep
regionoptional. If the model knows the country from context (a user in the UK, a receipt in euros) it will pass it; forcing it invites guesses. - Return the whole result object. Trimming it "for tokens" removes the confidence fields the model needs to answer honestly.
- Surface errors as tool errors with the API
message, so the model can tell the user "no credit" rather than "something went wrong".
[
{
"name": "scan_receipt",
"description": "Extract structured data from a photo of a receipt using the Tabscanner API. Uploads the image, waits for processing (usually a few seconds) and returns the result object: establishment, date, total, subTotal, tax, tip, discount, currency, paymentMethod, address, lineItems, summaryItems and confidence scores (0 to 1). Each call uses one credit. Call it once per receipt image. Pass the receipt's country as region for best accuracy. Trust total when validatedTotal is true; otherwise check totalConfidence and the arithmetic of lineItems.",
"input_schema": {
"type": "object",
"properties": {
"image_path": {
"type": "string",
"description": "Local path to a JPG or PNG photo of the receipt."
},
"region": {
"type": "string",
"description": "Two-letter lower-case ISO 3166-1 country code of the receipt, for example \"us\" or \"gb\". Optional but recommended."
},
"documentType": {
"type": "string",
"enum": ["receipt", "invoice", "auto"],
"description": "Type of document. Default \"receipt\". Use \"auto\" to let the API detect it."
},
"defaultDateParsing": {
"type": "string",
"enum": ["d/m", "m/d"],
"description": "How to read an ambiguous numeric date such as 02/03/2026: \"d/m\" is 2 March, \"m/d\" is 3 February."
}
},
"required": ["image_path"]
}
},
{
"name": "get_credit",
"description": "Return the number of Tabscanner credits left on the account. Each scan_receipt call uses one credit.",
"input_schema": {
"type": "object",
"properties": {}
}
}
]Reference implementation
A dependency-free Node module that implements both tools. scanReceipt uploads, polls once per second, and resolves with the result object plus the token and duplicate flag; it throws a TabscannerError carrying the API code and message for anything else. The module is part of the tested samples: run it directly with an image path to see the output.
To use it with the Claude API, register the tool definitions above and, when the model returns a tool_use block named scan_receipt, call scanReceipt(block.input) and return the JSON as the tool_result. For an MCP server, wrap the same two functions as MCP tools.
Porting to another language is a matter of taking the scan sample for that language from the Quickstart and returning the result instead of printing it.
// Tabscanner as an agent tool. scanReceipt() uploads, polls and returns the result object,
// so the model sees one call instead of a polling loop. getCredit() returns the balance.
// Node 20 or newer, no packages. Usage as a script: node agent-tool.js receipt.jpg [region]
import { openAsBlob } from 'node:fs';
import path from 'node:path';
const API_URL = process.env.TABSCANNER_API_URL ?? 'https://api.tabscanner.com';
const API_KEY = process.env.TABSCANNER_API_KEY;
const POLL_MS = Number(process.env.TABSCANNER_POLL_MS ?? 1000);
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
// Every response is HTTP 200; the outcome is the "code" field in the JSON body.
async function api(route, init = {}) {
const response = await fetch(`${API_URL}${route}`, { ...init, headers: { apikey: API_KEY } });
return response.json();
}
class TabscannerError extends Error {
constructor(body) {
super(`Tabscanner ${body.code}: ${body.message}`);
this.code = body.code;
this.retryable = body.code >= 500 && body.code !== 500;
}
}
/**
* Tool: scan_receipt. Returns the result object for one receipt image.
* @param {{ image_path: string, region?: string, documentType?: string, defaultDateParsing?: string }} input
*/
export async function scanReceipt(input, { timeoutMs = 60_000 } = {}) {
const form = new FormData();
const mimeType = input.image_path.toLowerCase().endsWith('.png') ? 'image/png' : 'image/jpeg';
form.append('file', await openAsBlob(input.image_path, { type: mimeType }), path.basename(input.image_path));
for (const key of ['region', 'documentType', 'defaultDateParsing']) {
if (input[key]) form.append(key, input[key]);
}
const upload = await api('/api/2/process', { method: 'POST', body: form });
if (upload.code !== 200 && upload.code !== 300) throw new TabscannerError(upload);
const deadline = Date.now() + timeoutMs;
while (Date.now() < deadline) {
await sleep(POLL_MS);
const body = await api(`/api/result/${upload.token}`);
if (body.code === 202) return { token: upload.token, duplicate: upload.duplicate, ...body.result };
if (body.code !== 301) throw new TabscannerError(body);
}
throw new Error(`Tabscanner: result for ${upload.token} not ready after ${timeoutMs} ms; poll it later`);
}
/** Tool: get_credit. Returns the number of credits left on the account. */
export async function getCredit() {
const body = await api('/api/credit');
if (typeof body !== 'number') throw new TabscannerError(body);
return body;
}
if (process.argv[1] && path.resolve(process.argv[1]) === new URL(import.meta.url).pathname) {
const result = await scanReceipt({ image_path: process.argv[2] ?? 'receipt.jpg', region: process.argv[3] });
console.log(JSON.stringify(result, null, 2));
}Which fields to trust
The result object carries a confidence between 0 and 1 for every important value, and boolean shortcuts for the highest tier. An agent answering questions about a receipt should:
- Report
totalwithout hedging whenvalidatedTotalistrue. Otherwise mention that the total was read withtotalConfidenceconfidence, and verify it: thelineItemsamounts should sum tosubTotal, andsubTotalplustax,tipandserviceChargesminusdiscountshould equaltotal. - Treat
establishmentas certain whenvalidatedEstablishmentistrue; it was cross-checked against the merchant's phone number or address. - Use
dateISOfor anything programmatic. There is no time zone; it is the local time printed on the receipt. - Prefer
descCleanoverdescfor item names. Aqtyof 0 means no quantity was printed, not zero items. - Read
currencyfrom the result rather than assuming it fromregion. - Say so when a field is
null; the receipt did not print it or it could not be read.
{
"token": "8f3c1e2ab7d94d1c9f0e5b6a2c4d7e81",
"duplicate": false,
"establishment": "Corner Deli & Grocery",
"validatedEstablishment": true,
"dateISO": "2026-03-14T12:41:00",
"total": 19.93,
"totalConfidence": 0.995,
"validatedTotal": true,
"subTotal": 18.45,
"tax": 1.48,
"currency": "USD",
"paymentMethod": "CASH",
"lineItems": [
{ "descClean": "TURKEY CLUB SANDWICH", "qty": 1, "lineTotal": 8.95 },
{ "descClean": "SPARKLING WATER 500ML", "qty": 2, "lineTotal": 7.00 },
{ "descClean": "BANANA", "qty": 0, "unit": 0.612, "lineTotal": 2.50 }
]
}AGENTS.md snippet
Paste this into the instructions file of a coding agent (AGENTS.md, CLAUDE.md, a system prompt) when it will work with the Tabscanner API directly. It is the shortest complete description of the API's behaviour.
## Tabscanner receipt OCR
Base URL `https://api.tabscanner.com`. Auth: header `apikey: <key>` (read it from `TABSCANNER_API_KEY`, never print it).
Flow: `POST /api/2/process` (multipart, one JPG/PNG in field `file`, optional `region`
two-letter country code) returns `{ token }`. Then `GET /api/result/{token}` once per second
until body `code` is 202; `result` holds the data. `GET /api/credit` returns a bare number.
Rules:
- Every response is HTTP 200. Read the numeric `code` in the JSON body, never the HTTP status.
- 200/300 = upload accepted (300 = image smaller than 720x1280, still processed).
- 301 = not ready, keep polling. 202 = done. Any other code = stop and report `message`.
- 4xx codes are permanent for the same input; do not retry. 5xx: retry with backoff. 500 = unreadable image.
- One credit per accepted upload. Re-uploading identical bytes returns `duplicate: true` and the original token.
- Prefer `validatedTotal`/`validatedEstablishment` = true. Otherwise compare `*Confidence` fields and check
that `lineItems` sum to `subTotal` and `subTotal + tax + tip + serviceCharges - discount = total`.
- Results and images are deleted after 90 days; store what you need.
Spec: https://docs.tabscanner.com/openapi.yaml · Full docs: https://docs.tabscanner.com/llms-full.txtMachine-readable resources
- openapi.yaml: OpenAPI 3.1. Every parameter and field has a description; the body codes are listed under
x-tabscanner-codeswith a recommended action each. Suitable for generating clients or for feeding to a model as context. - schemas/result.schema.json: JSON Schema 2020-12 for the result object, for validating tool results.
- llms.txt: a short index in the llms.txt format.
- llms-full.txt: the entire API reference as one Markdown file, generated from the reference page.
- Samples in eight languages live in the docs repository under
examples/, each with the same four programs (process, result, credit, scan), tested against a mock of the API.
These files are served from the same origin as this page and are updated together with it.