Tabscanner/API v2
Dashboard

Tabscanner API v2

Upload a photo of a receipt and get back structured JSON: merchant, date, totals, taxes, tip, payment method and every line item. A REST API with standard HTTP status codes, problem-details errors and safe retries.

Introduction

Tabscanner reads receipts. You send an image of a point-of-sale receipt or an invoice and receive a JSON object describing what is printed on it. Extraction combines layout-aware OCR with region-specific rules, and you can choose one of the Tabscanner models for the hardest documents.

How it works

  1. Upload. POST /v2/receipts with the image as multipart/form-data. The API queues it and immediately answers 202 Accepted with a token.
  2. Poll. GET /v2/receipts/{token} until status is done, waiting the number of seconds in the Retry-After header between calls. Standard processing takes a few seconds.
  3. Use the data. result contains the merchant, date, amounts, confidences and line items. Store what you need: uploads are deleted after the retention period.

Ground rules

  • Server-side only. Your API key spends your credit. Call the API from your backend, never from a mobile app or browser.
  • HTTPS only, at https://api3.tabscanner.com.
  • Trust the HTTP status. 2xx means success, 4xx means fix the request, 5xx means retry later. Every error has a JSON body with a stable code. See Errors.
  • Ignore fields you don't know. New fields are added without notice; see Versioning.

Getting an API key

Sign up at dashboard.tabscanner.com. Your key is shown under API details. Each accepted upload uses one credit; everything else is free.

Base URL
https://api3.tabscanner.com
Auth
Authorization: Bearer <key>
Upload
multipart/form-data, JPEG or PNG
Responses
JSON, standard HTTP status codes
Errors
application/problem+json (RFC 9457)
Retries
Idempotency-Key header
Spec
openapi.yaml · openapi.json · result.schema.json · llms.txt

Quickstart

The program on the right is the complete flow: upload, poll, print. Pick your language, put your key in TABSCANNER_API_KEY, and run it against a receipt photo.

  1. Export your key: export TABSCANNER_API_KEY=your-key
  2. Save the sample next to a receipt image called receipt.jpg
  3. Run it. After a few seconds it prints the merchant and the total.

Every sample on this page is a real file from the examples folder of this site, and each one is run against the API by an automated test before the docs are published. Samples read the base URL from TABSCANNER_API_URL only so the tests can point them at a test server; you can delete that line.

What the sample does

It uploads the image with region=us and a fresh Idempotency-Key, expects 202, then polls the result, sleeping for Retry-After seconds between calls, until status is done or failed. Any other HTTP status is printed with its error code and detail.

Set region to the two-letter country code of your receipts. It is optional but noticeably improves date, number and merchant extraction. See Regions and locales.

examples/curl/scan.sh
#!/usr/bin/env bash
# Full flow: upload a receipt, poll until the result is ready, print establishment and total.
# Needs curl and jq. Usage: TABSCANNER_API_KEY=your-key ./scan.sh receipt.jpg
set -euo pipefail
API_URL="${TABSCANNER_API_URL:-https://api3.tabscanner.com}"
IMAGE="${1:-receipt.jpg}"
AUTH="Authorization: Bearer ${TABSCANNER_API_KEY:?set TABSCANNER_API_KEY}"

# 1. Upload. 202 means accepted; anything else is an error described by the JSON body.
#    The Idempotency-Key makes it safe to resend this exact request after a network error.
upload=$(curl -sS -w '\n%{http_code}' "$API_URL/v2/receipts" \
  -H "$AUTH" -H "Idempotency-Key: $(uuidgen)" \
  -F "file=@$IMAGE" -F "region=us")
status=$(tail -n1 <<<"$upload"); body=$(sed '$d' <<<"$upload")
if [ "$status" != 202 ]; then
  echo "Upload failed: $status $(jq -r '.code + ": " + (.detail // .title)' <<<"$body")" >&2; exit 1
fi
token=$(jq -r .token <<<"$body")

# 2. Poll until done or failed, waiting as long as Retry-After says between calls.
deadline=$((SECONDS + 120))
while :; do
  headers=$(mktemp)
  body=$(curl -sS -D "$headers" -w '\n%{http_code}' "$API_URL/v2/receipts/$token" -H "$AUTH")
  status=$(tail -n1 <<<"$body"); body=$(sed '$d' <<<"$body")
  wait=$(awk -F': ' 'tolower($1)=="retry-after" {print $2+0}' "$headers"); rm -f "$headers"
  if [ "$status" != 200 ]; then
    echo "Result failed: $status $(jq -r '.code + ": " + (.detail // .title)' <<<"$body")" >&2; exit 1
  fi
  case $(jq -r .status <<<"$body") in
    done) break ;;
    failed) echo "Processing failed: $(jq -r .error.detail <<<"$body")" >&2; exit 1 ;;
  esac
  [ "$SECONDS" -lt "$deadline" ] || { echo "Timed out waiting for the result" >&2; exit 1; }
  sleep "${wait:-2}"
done

# 3. Use the data.
jq -r '"Establishment: \(.result.establishment)", "Total: \(.result.total) \(.result.currency)"' <<<"$body"

Authentication

Send your API key as a bearer token on every request:

Authorization: Bearer YOUR_API_KEY

An apikey: YOUR_API_KEY header is also accepted, so existing integrations can switch hosts without changing how they send the key. Keys are never accepted in the URL or query string.

A missing, unknown or revoked key returns 401 Unauthorized with code API_KEY_INVALID and a WWW-Authenticate: Bearer header.

Keeping the key safe

  • Store it in an environment variable or a secrets manager, never in source control.
  • Call the API from a backend service. A key embedded in a mobile or web app can be extracted and used against your credit.
  • Accounts can hold more than one key, so you can rotate without downtime: create a new key in the dashboard, deploy it, then revoke the old one.
curl https://api3.tabscanner.com/v2/account \
  -H "Authorization: Bearer $TABSCANNER_API_KEY"

Requests and responses

The API follows standard HTTP semantics. Branch on the status code first, then read the body.

StatusMeaning
200 OKThe request succeeded. For a result, check status: pending is normal while processing.
202 AcceptedAn upload was queued. Its result is at the Location URL.
4xxSomething is wrong with the request. Fix it before retrying. 429 and 409 are the exceptions: wait and retry.
5xxA temporary problem on our side. Retry with backoff.

Error bodies

Every error response has Content-Type: application/problem+json and the RFC 9457 problem-details shape, plus two extensions: code, a stable identifier to branch on, and requestId.

typestringrequired

URI identifying the error type, urn:tabscanner:error:<code>.

titlestringrequired

Short, human-readable summary of the error type.

statusintegerrequired

The HTTP status code, repeated for convenience.

detailstring

Explanation specific to this occurrence. Not always present.

instancestringrequired

The request path.

codestringrequired

Stable, machine-readable error code. See the error reference.

One of: FORM_PARSER, IDEMPOTENCY_KEY_INVALID, MODEL_UNKNOWN, NO_FILE, TOO_MANY_FILE_FIELDS, TOO_MANY_FILES, VALIDATION, API_KEY_INVALID, INSUFFICIENT_CREDIT, SUBSCRIPTION_INACTIVE, SUBSCRIPTION_NOT_FOUND, MODEL_NOT_PERMITTED, PDF_NOT_PERMITTED, NOT_FOUND, TOKEN_NOT_FOUND, IDEMPOTENCY_IN_PROGRESS, FILE_TOO_LARGE, UNSUPPORTED_EXTENSION, UNSUPPORTED_FILE_TYPE, UNSUPPORTED_MIME_TYPE, IMAGE_PROCESSING, RATE_LIMITED, DB_QUERY, INTERNAL, RESULT_INVALID, DB_CONNECTION, QUEUE, SERVICE_UNAVAILABLE, STORAGE

requestIdstringrequired

Identifier of this request. Quote it when contacting support.

Branch on code, never on title or detail: those are written for people and may change. The full list is under Errors.

Request IDs

Every response carries an X-Request-Id header, and error bodies repeat it as requestId. You can send your own X-Request-Id (up to 128 letters, digits, _ . : -) to correlate our logs with yours; otherwise one is generated. Quote it when you contact support.

Caching

Responses are never cacheable (Cache-Control: no-store). Store results in your own systems.

HTTP/1.1 402 Payment Required
Content-Type: application/problem+json
X-Request-Id: 8e395308b68c2e6f88663446b8708121

{
  "type": "urn:tabscanner:error:insufficient-credit",
  "title": "Not enough credit",
  "status": 402,
  "instance": "/v2/receipts",
  "code": "INSUFFICIENT_CREDIT",
  "requestId": "8e395308b68c2e6f88663446b8708121"
}

Upload a receipt

POST/v2/receipts

Queues a receipt for processing and returns immediately with 202 Accepted and a token. Each accepted upload uses one credit. Rejected uploads (any 4xx or 5xx) are not charged.

Form fields

Send multipart/form-data. Each field may be sent once.

filefilerequired

The receipt image, JPEG or PNG, up to 30 MB. Accounts with multi-image uploads enabled can send up to three parts named file for one long receipt photographed in sections; they are processed as a single document.

regionstring

Two-letter ISO 3166-1 country code of the receipt, lower case. Selects number and date formats, language and regional extraction rules. Strongly recommended.

One of: ar, au, be, br, ca, cl, co, fr, de, gr, hk, in, id, ie, it, ja, ke, my, mx, nz, pa, pe, ph, sg, za, es, se, ch, to, ae, gb, uy, us, vn

documentTypestring

Type of document in the image. auto detects it and reports it in result.documentType.

One of: receipt, invoice, auto. Default: receipt

modelstring

Process with one of the Tabscanner models instead of standard OCR. Omit for standard OCR. Available to every account; the -pro models need the Pro package (features.proModels on GET /v2/account). See Models.

One of: harajuku, harajuku-pro

decimalPlacesinteger

How many decimal places amounts are printed with (for example 3 for KWD or BHD). A parsing hint, not an output format.

Range: 0 to 3

defaultDateParsingstring

How to read an ambiguous date such as 02/03/2026: d/m reads 2 March, m/d reads 3 February.

One of: d/m, m/d

centsboolean

Read amounts printed without a decimal separator as minor units. Only with decimalPlaces=3 (245 becomes 0.245). Send true or false.

Default: false

Headers

Idempotency-Keystringrecommended

A unique value per upload, such as a UUID. If the request is retried with the same key within 24 hours, the original response is returned (with Idempotent-Replayed: true) and nothing is uploaded or charged again. See Retries and idempotency.

Response

202 Accepted, with a Location header pointing at the result and a Retry-After header giving the seconds to wait before the first poll.

tokenstring

Identifies the upload. Pass it to GET /v2/receipts/{token}.

statusstring

Always queued.

Always queued

duplicateboolean

true when a visually identical image was uploaded before on this account.

duplicateTokenstring

Token of the earlier upload. Present only when duplicate is true.

warningsarray of object

Non-fatal issues with the upload. Currently LOW_RESOLUTION: an image is smaller than 720 by 1280 pixels.

Duplicates are still processed

duplicate: true tells you a visually identical image was uploaded before, and duplicateToken names that upload. The new upload is processed and charged as normal. To avoid paying twice for a retried request, use an Idempotency-Key instead.

Errors

400 for a malformed form, missing file or unknown model; 402 when out of credit; 403 when model is not included in your plan; 413 when a file is too large; 415 for anything other than JPEG or PNG. See Errors for every code.

examples/curl/upload.sh
#!/usr/bin/env bash
# Upload a receipt. Prints the 202 response: the token, the duplicate flag and the result link.
# Usage: TABSCANNER_API_KEY=your-key ./upload.sh receipt.jpg
set -euo pipefail
API_URL="${TABSCANNER_API_URL:-https://api3.tabscanner.com}"

curl -sS --fail-with-body "$API_URL/v2/receipts" \
  -H "Authorization: Bearer ${TABSCANNER_API_KEY:?set TABSCANNER_API_KEY}" \
  -H "Idempotency-Key: $(uuidgen)" \
  -F "file=@${1:-receipt.jpg}" \
  -F "region=us"
HTTP/1.1 202 Accepted
Location: /v2/receipts/TC72MdZUW69PpPy9
Retry-After: 2

{
  "token": "TC72MdZUW69PpPy9",
  "status": "queued",
  "duplicate": false,
  "warnings": [],
  "links": { "result": "/v2/receipts/TC72MdZUW69PpPy9" }
}

Get a result

GET/v2/receipts/{token}

Returns the state of an upload and, once processed, the extracted data. Free.

The response is 200 OK in all three states. status tells you which one you are in:

  • pending: still processing. Call again after the number of seconds in Retry-After.
  • done: result holds the Result object. Results do not change once done, so you can fetch them again safely.
  • failed: the document could not be read, for example because the photo is not a receipt or is unreadable. Retrying the same image will fail the same way; ask for a new photo.
tokenstringrequired

The token from the upload.

statusstringrequired

pending: still processing, call again after Retry-After. done: result is present. failed: the document could not be processed.

One of: pending, done, failed

resultResult

errorobject

Present when status is failed.

  • code
  • detail

An unknown token, one belonging to another account, or one older than the retention period returns 404 with code TOKEN_NOT_FOUND.

examples/curl/result.sh
#!/usr/bin/env bash
# Fetch a result once. status is "pending" (call again after Retry-After), "done" or "failed".
# Usage: TABSCANNER_API_KEY=your-key ./result.sh TOKEN
set -euo pipefail
API_URL="${TABSCANNER_API_URL:-https://api3.tabscanner.com}"

curl -sS --fail-with-body "$API_URL/v2/receipts/${1:?token}" \
  -H "Authorization: Bearer ${TABSCANNER_API_KEY:?set TABSCANNER_API_KEY}"
HTTP/1.1 200 OK
Retry-After: 2

{ "token": "TC72MdZUW69PpPy9", "status": "pending" }

Get your account

GET/v2/account

Remaining credit, billing type, retention period and the features enabled for your key. Free; call it before a batch to check you have enough credit.

creditnumber

Remaining credit. Each accepted upload uses one.

billingstring

credit: prepaid credit is decremented per upload. subscription: uploads are billed to a subscription.

One of: credit, subscription

dataRetentionDaysintegernullable

Days that uploads and results are kept before deletion.

featuresobject

Features enabled for this account.

  • multipleImages Up to three images may be sent in one upload.
  • pdf PDF uploads are accepted. Currently always false.
  • otherData result.otherData is populated.
  • models The model upload field may be used. True for every account.
  • proModels The -pro models may be used. True on paid plans with the Pro package.
examples/curl/account.sh
#!/usr/bin/env bash
# Show the account: remaining credit, billing type and enabled features.
# Usage: TABSCANNER_API_KEY=your-key ./account.sh
set -euo pipefail
API_URL="${TABSCANNER_API_URL:-https://api3.tabscanner.com}"

curl -sS --fail-with-body "$API_URL/v2/account" \
  -H "Authorization: Bearer ${TABSCANNER_API_KEY:?set TABSCANNER_API_KEY}"

List regions

GET/v2/regions

The regions with dedicated extraction configurations, as {"data": [{"region", "name"}]}. Pass a region's code, in lower case, as the region form field. Free.

curl https://api3.tabscanner.com/v2/regions \
  -H "Authorization: Bearer $TABSCANNER_API_KEY"

List custom fields

GET/v2/custom-fields

The custom fields configured per region, as {"data": [{"name", "domain", "region"}]}. name is the key that appears under result.customFields. Free. See Custom fields.

curl https://api3.tabscanner.com/v2/custom-fields \
  -H "Authorization: Bearer $TABSCANNER_API_KEY"

Result object

Everything Tabscanner read from the document. Amounts are decimal numbers in the receipt's currency. Confidence values run from 0 (no confidence) to 1 (certain). A field the receipt does not print is empty, null or 0; check its confidence rather than the value. New fields may appear at any time.

establishmentstringnullable

Name of the merchant. Detected by machine learning and, where configured, custom establishment lookups.

establishmentConfidencenumbernullable

Confidence that establishment is correct, 0 to 1.

Range: 0 to 1

validatedEstablishmentboolean

true when the establishment was cross-referenced with the phone number or address on the receipt and confirmed in Tabscanner's database.

datestringnullable

Purchase date and time as YYYY-MM-DD hh:mm:ss. Time is 00:00:00 when not printed.

dateISOstringnullable

Purchase date and time as ISO 8601 YYYY-MM-DDThh:mm:ss.

dateConfidencenumbernullable

Confidence that date is correct, 0 to 1.

Range: 0 to 1

totalnumbernullable

Grand total paid.

totalConfidencenumbernullable

Confidence that total is correct and is the total, 0 to 1.

Range: 0 to 1

validatedTotalboolean

true when totalConfidence is at least 0.99.

subTotalnumbernullable

Amount before tax, tip and service charges.

subTotalConfidencenumbernullable

Confidence that subTotal is correct, 0 to 1.

Range: 0 to 1

validatedSubTotalboolean

true when subTotalConfidence is at least 0.99.

taxnumbernullable

Total tax amount.

taxesarray of number

Each tax amount found, in receipt order.

taxesConfidencearray of number

Confidence for each entry of taxes, same order.

tipnumbernullable

Tip or gratuity amount.

tipConfidencenumbernullable

Confidence that tip is correct, 0 to 1.

Range: 0 to 1

serviceChargesarray of number

Each service charge amount found.

serviceChargeConfidencesarray of number

Confidence for each entry of serviceCharges, same order.

discountnumbernullable

Total discount applied to the receipt.

discountsarray of number

Each discount amount found.

discountConfidencesarray of number

Confidence for each entry of discounts, same order.

roundingnumbernullable

Cash rounding applied to the total, if any.

roundingConfidencenumbernullable

Confidence that rounding is correct, 0 to 1.

Range: 0 to 1

cashnumbernullable

Cash tendered.

cashConfidencenumbernullable

Confidence that cash is correct, 0 to 1.

Range: 0 to 1

changenumbernullable

Change returned to the customer.

changeConfidencenumbernullable

Confidence that change is correct, 0 to 1.

Range: 0 to 1

currencystringnullable

Detected ISO 4217 currency code. Known values include USD, EUR, GBP, AED, CHF, AUD, HKD, JPY, KRW, RMB, BRL, CAD, ZAR.

paymentMethodstringnullable

Payment method as printed. Known values include VISA, Mastercard, American Express, Discover, ALIPAY, WE CHAT, CASH, Debit.

addressstringnullable

Merchant address text exactly as extracted, not normalised.

addressNormAddressNormnullable

phoneNumberstringnullable

Merchant phone number as printed, not normalised.

urlstringnullable

Website address printed on the receipt.

barcodesarray of array

Barcodes found on the receipt. Each entry is a two-element array of [data, type]. Types include EAN-13/UPC-A, UPC-E, EAN-8, Code 128, Code 39, Interleaved 2 of 5 and QR Code.

expenseTypestringnullable

Beta. Expense classification, for example Meals/Individual Meals while Traveling, Transportation-Rideshare/Uber/Lyft/Taxi, Travel Expenses/Hotel.

documentTypestringnullable

The document type. The value sent as documentType (default receipt), or the detected type, Receipt or Invoice, when documentType=auto was sent.

customFieldsobject

Account-specific extracted fields, for example Country, CardLast4Digits, ReceiptNumber, VATNumber. Keys depend on your configuration.

otherDataarray of any

Additional raw data. Only populated for accounts with this feature enabled (features.otherData on GET /v2/account); otherwise an empty array.

lineItemsarray of LineItem

Product lines found on the receipt, top to bottom.

summaryItemsarray of LineItem

Non-product lines such as Total, Sub Total, Tax, Cash, Change, Tip and Service Charge, as LineItem objects with lineType set.

AddressNorm object

buildingstringnullable

Building name or unit.

numberstringnullable

Street number.

streetstringnullable

Street name.

suburbstringnullable

Suburb or district.

citystringnullable

City or town.

statestringnullable

State, province or region.

postcodestringnullable

Postal code.

countrystringnullable

Country.

LineItem object

One printed line: a product in lineItems, or a totals-block line in summaryItems. See the Line items guide.

descstringnullable

Text found on the same printed line as lineTotal.

descCleanstringnullable

Consolidated, cleaned description including text from adjacent lines that belong to this item, with prices and discounts removed.

lineTotalnumbernullable

Amount at the end of the line.

pricenumbernullable

Unit price when printed separately from lineTotal.

qtynumbernullable

Quantity when printed. Defaults to 0 when no quantity is found; a value of 1 means a 1 was actually printed.

unitstring

Unit of measure for items sold by weight or volume, as printed, for example kg, lbs or l. Empty string when no unit is printed.

productCodestringnullable

SKU, PLU or barcode number found on the line.

symbolsarray of string

Symbols printed after the amount, typically tax codes such as A, B, *.

supplementaryLineItemsobjectnullable

Present only when text above or below the line could not be resolved into descClean. Contains the unresolved text.

discountnumbernullable

Discount applied to this line, when printed.

customFieldsobject

Account-specific fields extracted for this line. Empty unless configured for your account.

lineTypestringnullable

Classification of the line. For summaryItems one of Total, SubTotal, Tax, TotalTax, Cash, Change, Tip, ServiceCharge. For product lines this is Product or null.

Worked example

A café receipt from London, uploaded with region=gb. The response on the right is abridged: the confidence fields for amounts that were not printed, and empty arrays, are left out.

  • date is 8 October: with region=gb the printed 08/10/2026 is read day first.
  • subTotal + serviceCharges = total (35.75 + 4.47 = 40.22). The VAT is included in the prices, so it is reported in tax but not added again.
  • The line totals sum to subTotal: 7.00 + 9.50 + 11.25 + 3.80 + 4.20 = 35.75.
  • qty is 2 for the coffees because 2 x is printed; price is the unit price.
{
  "token": "dv9aLY1MVThnSJRo",
  "status": "done",
  "result": {
    "establishment": "GREENLEAF CAFE",
    "establishmentConfidence": 0.8,
    "address": "12 Market Street, London EC1A 1BB",
    "addressNorm": { "number": "12", "street": "Market Street", "city": "London", "postcode": "EC1A 1BB", "country": "GB" },
    "phoneNumber": "020 7946 0321",
    "date": "2026-10-08 12:41:00",
    "dateISO": "2026-10-08T12:41:00",
    "dateConfidence": 0.95,
    "currency": "GBP",
    "total": 40.22,
    "totalConfidence": 0.99,
    "subTotal": 35.75,
    "tax": 5.96,
    "taxes": [5.96],
    "serviceCharges": [4.47],
    "paymentMethod": "VISA",
    "lineItems": [
      { "qty": 2, "descClean": "Flat White", "price": 3.5, "lineTotal": 7.0 },
      { "qty": 1, "descClean": "Avocado Toast", "price": 9.5, "lineTotal": 9.5 },
      { "qty": 1, "descClean": "Club Sandwich", "price": 11.25, "lineTotal": 11.25 },
      { "qty": 1, "descClean": "Orange Juice", "price": 3.8, "lineTotal": 3.8 },
      { "qty": 1, "descClean": "Chocolate Brownie", "price": 4.2, "lineTotal": 4.2 }
    ],
    "summaryItems": [
      { "lineType": "SubTotal", "descClean": "Subtotal", "lineTotal": 35.75 },
      { "lineType": "Tax", "descClean": "VAT 20% (incl.)", "lineTotal": 5.96 },
      { "lineType": "ServiceCharge", "descClean": "Service 12.5%", "lineTotal": 4.47 },
      { "lineType": "Total", "descClean": "TOTAL", "lineTotal": 40.22 }
    ],
    "customFields": { "CardLast4Digits": "4417", "VATNumber": "GB 123 4567 89" },
    "documentType": "receipt"
  }
}

Models

By default receipts are read by Tabscanner's standard OCR, which is fast and handles the vast majority of receipts well. For harder documents, send model with the upload to have it read by one of the Tabscanner models instead.

modelProfileUse it for
omittedStandard OCRThe default. Fastest turnaround, layout-aware OCR with regional rules.
harajukuModel processingReads the document more thoroughly than standard OCR, for long, crumpled, faded or unusually laid out receipts and invoices.
harajuku-proModel processing, proA more thorough version of harajuku, for documents where accuracy matters most. Longer processing time.
  • Same request, same result. Only the model field changes. The response and the Result object are identical in shape whichever model you choose.
  • Slower. Model processing typically takes 10 to 20 seconds instead of a few. Keep polling as normal; Retry-After paces you.
  • Exact names. Values are lower case and case-sensitive. An unknown value returns 400 with code MODEL_UNKNOWN and the list of valid models, before anything is charged.
  • Credit. An upload uses one credit whichever model reads it.
  • Plans. harajuku is available to every account. The -pro models need the Pro package: check features.proModels on GET /v2/account. Without it, a request with a -pro model returns 403 with code MODEL_NOT_PERMITTED, before anything is charged; the same upload with the base model, or without model, works as normal.

When to reach for a model: long or multi-section receipts, faded or crumpled paper, unusual layouts, handwritten additions, or when standard OCR misses line items or splits totals on documents that matter to you. Try the same images with and without model and compare.

curl https://api3.tabscanner.com/v2/receipts \
  -H "Authorization: Bearer $TABSCANNER_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -F "[email protected]" \
  -F "region=gb" \
  -F "model=harajuku"

Polling and timeouts

Processing is asynchronous. A well-behaved client:

  • Stores the token immediately after the upload, so a crash after this point cannot lose the credit that was spent.
  • Waits Retry-After seconds before each poll, both after the upload and after each pending response. Polling faster does not make processing faster.
  • Stops on done or failed, or on any non-200 status.
  • Gives up after about 60 seconds (120 when using a model) and reports a timeout. The token stays valid, so a later poll can still collect the result.

For batches, upload everything first, recording each token, then poll the tokens in turn. Uploads are cheap; waiting is not. A handful of concurrent connections is plenty.

Polling only works while the result is within the retention period; after that the token returns 404.

Retries and idempotency

Networks fail. The question after a failed upload is always: did it go through? Idempotency-Key answers it. Generate a unique key per receipt, such as a UUID, send it with the upload, and if you get no response, a 5xx or a 409, send the same request with the same key again:

  • If the first attempt succeeded, you receive its original 202 response (with Idempotent-Replayed: true). Nothing is uploaded or charged twice.
  • If the first attempt failed, the key is released and the retry is processed normally.
  • If the first attempt is still in progress, you receive 409 with code IDEMPOTENCY_IN_PROGRESS. Wait a second and retry.

Keys are scoped to your account and remembered for 24 hours. Reusing a key for a different image within that time returns the first image's response, so use a new key per receipt.

Retry policy

  • Network errors, 5xx, 409, 429: retry with exponential backoff (1, 2, 4, 8 seconds, then give up), honouring Retry-After when present. For uploads, reuse the same Idempotency-Key.
  • Other 4xx: do not retry unchanged. Fix the request, top up credit (402) or accept that the token is gone (404).
  • status: failed: the image itself is the problem; uploading the same bytes again will fail the same way. Ask for a new photo.

Duplicates

Separately from idempotency, every upload is fingerprinted. When a visually identical image has been uploaded before on your account, the response has duplicate: true and duplicateToken set to the earlier upload. This is a signal, not a block: the new upload is processed and charged. Use it to spot the same receipt being submitted twice, for example in expense claims. Uploads with several images are never marked as duplicates.

Errors

Every error is a problem-details body with a stable code. This is the complete list.

StatusCodeWhenWhat to do
400FORM_PARSER
Malformed multipart request
The body is not valid multipart/form-data, or exceeds the part or field limits.Fix the request encoding. Do not retry unchanged.
400IDEMPOTENCY_KEY_INVALID
Invalid Idempotency-Key header
The Idempotency-Key header is empty, too long or contains invalid characters.Use a printable ASCII value of up to 255 characters, such as a UUID.
400MODEL_UNKNOWN
Unknown model
The model form field is not one of the available models.Use one of the values listed in the error detail, or omit model.
400NO_FILE
No file detected
The request contains no file part.Send the image as a part named file.
400TOO_MANY_FILE_FIELDS
Too many files
Files were sent under more than three different field names.Send every image as a part named file.
400TOO_MANY_FILES
Too many files
More than one image was sent and multi-image uploads are not enabled for the account, or more than three were sent.Send one image, or up to three if features.multipleImages is true.
400VALIDATION
Request validation failed
A path parameter or header is malformed, for example a token with invalid characters.Fix the request. Do not retry unchanged.
401API_KEY_INVALID
Missing or invalid API key
No API key was sent, or the key is unknown or revoked.Check the Authorization header. Do not retry with the same key.
402INSUFFICIENT_CREDIT
Not enough credit
The account has no credit left.Top up in the dashboard. Do not retry until credit is available.
402SUBSCRIPTION_INACTIVE
Subscription not active
The account’s subscription is not active.Renew the subscription in the dashboard.
402SUBSCRIPTION_NOT_FOUND
Subscription not found
The account references a subscription that does not exist.Contact support.
403MODEL_NOT_PERMITTED
This model is not available on your plan
A -pro model was requested, but the account does not have the Pro package.Use the base model instead of the -pro one, omit model for standard OCR, or upgrade. features.proModels on GET /v2/account tells you in advance.
403PDF_NOT_PERMITTED
PDF uploads are not enabled for this account
A PDF was uploaded. PDF uploads are not currently available.Convert the page to JPEG or PNG and upload that.
404NOT_FOUND
Route not found
The method and path do not match any endpoint.Check the URL against this reference.
404TOKEN_NOT_FOUND
Token not found
The token does not exist, belongs to another account, or is older than the retention period.Check the token. Do not retry.
409IDEMPOTENCY_IN_PROGRESS
A request with this Idempotency-Key is still in progress
An earlier request with the same Idempotency-Key is still being processed.Wait a second and retry; you will then receive the original response.
413FILE_TOO_LARGE
File exceeds the maximum upload size
A file is larger than 30 MB.Send a smaller image. Phone photos are well under this limit.
415UNSUPPORTED_EXTENSION
Unsupported file extension
The file name does not end in .jpg, .jpeg or .png.Name the multipart file with the matching extension.
415UNSUPPORTED_FILE_TYPE
Unsupported file type
The file content is not an image.Send a JPEG or PNG.
415UNSUPPORTED_MIME_TYPE
Unsupported MIME type
The file is an image, but not JPEG or PNG (for example GIF or WebP).Convert to JPEG or PNG.
422IMAGE_PROCESSING
Unable to process image
The image could not be decoded, for example a truncated or corrupt file.Check the file opens locally, then upload it again.
429RATE_LIMITED
Too many requests
More than 50 requests per second from one IP address, after a burst allowance of 200.Wait for the Retry-After seconds, then continue at a lower rate.
500DB_QUERY
Database error
An unexpected internal error.Retry once; if it persists, contact support with the requestId.
500INTERNAL
Internal server error
An unexpected internal error.Retry once; if it persists, contact support with the requestId.
500RESULT_INVALID
Stored result is not valid JSON
The stored result for this token is corrupt.Contact support with the token and requestId.
503DB_CONNECTION
Database unavailable
A temporary internal outage.Retry with exponential backoff.
503QUEUE
Processing queue unavailable
The processing queue is temporarily unavailable. Nothing was charged.Retry with exponential backoff, using the same Idempotency-Key.
503SERVICE_UNAVAILABLE
Service temporarily unavailable
The API is restarting or unreachable.Retry with exponential backoff.
503STORAGE
File storage unavailable
Image storage is temporarily unavailable. Nothing was charged.Retry with exponential backoff, using the same Idempotency-Key.

A failed upload is never charged. A pending result is not an error, and status: failed on a result is a successful response about a document that could not be read; see Get a result.

Regions and locales

The region form field tells Tabscanner which country the receipt comes from. It selects the date and number formats to expect, the language rules and the regional extraction rules, for example how VAT lines are labelled. Without it the API has to guess from the image, which works most of the time but is less accurate on ambiguous receipts.

Use the lower-case two-letter ISO 3166-1 country code. Supported values:

ar au be br ca cl co fr de gr hk in id ie it ja ke my mx nz pa pe ph sg za es se ch to ae gb uy us vn

GET /v2/regions returns the same list with names. If your receipts come from a country that is not listed, contact [email protected]. The currency in the result is detected from the receipt itself and is independent of region.

Dates and numbers

Ambiguous dates

A date printed as 02/03/2026 is 2 March in most of the world and 3 February in the United States. The API resolves this from region when it can; defaultDateParsing sets the order explicitly: d/m for day first, m/d for month first. When both are given, defaultDateParsing wins.

The result carries the date twice: date as YYYY-MM-DD hh:mm:ss and dateISO as YYYY-MM-DDThh:mm:ss. Neither has a time zone; it is the local time printed on the receipt.

Decimal places

Most currencies print two decimal places. Some, such as the Kuwaiti or Bahraini dinar, print three, and some receipts print none. decimalPlaces tells the number parser what to expect. It is a hint for reading the receipt, not an output format: amounts in the response are always plain decimal numbers.

Amounts printed without a separator

Set cents=true together with decimalPlaces=3 when receipts print amounts as whole minor units, so that 245 is read as 0.245 while 1.574 stays 1.574.

Cash rounding

Where small coins have been withdrawn, cash totals are rounded to the nearest 5 or 10 minor units. When a rounding line is printed, the amount appears in rounding, and total is the rounded amount actually paid.

Line items

Receipts rarely print one product per line. Descriptions wrap, quantities and unit prices sit on the line above the amount, discounts appear below, and tax codes trail the price. Tabscanner resolves these relationships and returns one LineItem per product.

  • desc is the text on the same printed line as the amount, as read.
  • descClean merges the description across adjacent lines and strips prices, quantities and discount text. Use it for display and matching.
  • qty and price are filled when printed. A missing quantity is 0, not assumed to be 1, so you can tell "printed 1" from "not printed".
  • unit is the unit of measure for items sold by weight or volume, such as kg, lbs or l; empty when none is printed.
  • symbols keeps the tax-class letters printed after the amount, useful for VAT reporting.

Summary items

summaryItems lists the lines of the totals block, each with a lineType such as SubTotal, Tax, Total, Cash, Change, Tip or ServiceCharge. The top-level amounts are derived from these lines, so use the top-level fields for values and the summary items when you need the printed label or order.

Checking the arithmetic

The lineTotals of lineItems should sum to subTotal, and subTotal plus taxes not already included, tip and service charges, minus discounts, should equal total. When they do not, a line was probably missed or merged; the confidence fields tell you which side to trust, and a model often resolves it.

Multiple images

A receipt too long for one photo can be sent as up to three images in one upload, in top-to-bottom order, all as parts named file. They are read as a single document with a single result and use one credit.

This is enabled per account: check features.multipleImages on GET /v2/account. Without it, more than one image returns 400 with code TOO_MANY_FILES. Where possible, prefer a single tall photo; see Image guidance.

curl https://api3.tabscanner.com/v2/receipts \
  -H "Authorization: Bearer $TABSCANNER_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -F "[email protected]" \
  -F "[email protected]" \
  -F "[email protected]"

Custom fields

Beyond the standard result, Tabscanner can extract fields specific to your use case and return them in customFields, for example a receipt number, merchant ID, VAT number, loyalty card number, country, or the last four digits of the payment card.

Custom fields, merchant-specific configurations and custom training are configured by the Tabscanner team for your account, so customFields may be empty until something has been set up. GET /v2/custom-fields lists the fields available per region. Contact [email protected] with a handful of sample receipts and the fields you need.

Image guidance

Accuracy depends on the photo more than anything else. Extraction is robust to skew, shadows and low contrast, but it cannot read what is not in the frame or what is blurred beyond recognition. Aim for:

  • At least 720 by 1280 pixels. Modern phone cameras exceed this by default; do not downscale before upload. Smaller images are processed, with a LOW_RESOLUTION warning in the upload response.
  • The whole receipt in frame, including the header at the top and the totals block at the bottom.
  • Flat and still. Put the receipt on a flat surface, hold the phone steady and let the camera focus.
  • Even light, without flash glare on thermal paper.
  • Original JPEG or PNG. Screenshots of e-receipts work well. Heavily compressed or re-saved images lose fine print.

If you control the capture experience, a guided camera overlay that shows the receipt edges and only takes the shot when the image is sharp raises accuracy more than any server-side setting.

Four receipt photos marked with a red cross: blurred, crumpled, cut off at the edges, and hand-held at an angle
Avoid: camera shake, crumpled paper, cut-off edges and hand-held shots at an angle.
Three receipt photos to avoid: pen markings, over-exposure and heavy shadow, and one good photo of a flat, evenly lit receipt marked with a green tick
Avoid pen marks, over-exposure and shadows. Bottom right: a well exposed, flat, fully framed receipt.

Limits and rate limits

Formats
JPEG and PNG. PDF is not currently supported.
File size
Up to 30 MB per image, 50 MB per request.
Images per upload
One, or up to three with multiple images enabled.
Image size
720 by 1280 pixels or larger recommended.
Processing time
A few seconds with standard OCR, typically 10 to 20 seconds with a model.
Credits
One per accepted upload. Results, account, regions and custom fields are free. Rejected uploads are not charged.
Rate limit
50 requests per second per IP address, with bursts of up to 200.
Retention
Uploads and results are deleted after your account's retention period (dataRetentionDays on GET /v2/account, normally 90 days).

Requests over the rate limit receive 429 Too Many Requests with a Retry-After header. Polling at the pace Retry-After sets keeps you far below the limit. If you need sustained higher throughput, contact support.

Security and data

  • Transport. HTTPS only.
  • At rest. Uploaded images and extracted results are encrypted at rest.
  • Retention. Images and results are deleted at the end of your retention period. Persist anything you need in your own systems.
  • Keys. Shown in the dashboard and revocable at any time. Multiple keys per account are supported. Keys are only accepted in headers, never in URLs, so they do not end up in proxy or server logs.
  • Client side. Never call the API from a device you do not control. Proxy through your backend.

For data processing agreements, hosting location and deletion requests, contact [email protected].

Versioning

The major version is the first segment of every path: /v2/. Within a version, changes are additive: new endpoints, new optional fields, new result fields, new error codes and new models may appear at any time. Write clients that ignore unknown fields and handle unknown error codes by their HTTP status.

Removals, renames, type changes and behavioural changes only happen in a new major version, and the previous version keeps working for existing integrations. Changes are listed in the changelog.

The original API (/api/2/process and /api/result/{token}) remains available and is documented at docs.tabscanner.com.

AI agents and LLMs

Building an assistant or agent that reads receipts? The AI agents page has ready-made tool definitions, a reference implementation that hides the polling, the rules an agent should follow, and guidance on which fields to trust.

Machine-readable versions of this documentation:

Support

Email [email protected] for technical questions, region requests, custom fields and anything not answered here. Include the requestId of a failing request, or the token of a problematic upload, and if you can, the image, so we can reproduce it.