Tabscanner/Docs
Dashboard

Tabscanner API

Upload a photo of a receipt and get back structured JSON: merchant, date, totals, taxes, tip, payment method and every line item. Two calls, one API key, any language.

Introduction

Tabscanner is a receipt OCR service. You send an image of a point-of-sale receipt and receive a JSON object describing what is printed on it. The extraction combines layout-aware OCR with region-specific rules, so it handles many languages, currencies and receipt formats.

How it works

  1. Upload. Send one JPG or PNG image to POST /api/2/process. The API queues it and immediately returns a token.
  2. Poll. Call GET /api/result/{token} until the body reports code 202. Processing normally takes a few seconds.
  3. Use the data. The result object contains the merchant, date, amounts, confidences and line items. Store what you need; results are kept for 90 days.

Ground rules

  • Server-side only. Your API key grants access to your account and credit. Call the API from your backend, never from a mobile app or browser. That is why there are no Swift, Kotlin or browser samples here.
  • TLS only. The API is served over HTTPS at https://api.tabscanner.com. There is no plain HTTP endpoint.
  • Read the body code, not the HTTP status. Every response is HTTP 200. The outcome of a call is the numeric code in the JSON body. See Responses and codes.
  • One image per call. JPG or PNG, ideally 720 by 1280 pixels or larger.

Getting an API key

Sign up at dashboard.tabscanner.com. Your key is shown under API details. The Starter plan includes free monthly credits, and each processed image uses one credit.

Base URL
https://api.tabscanner.com
Auth
apikey request header
Upload
multipart/form-data, one JPG or PNG
Responses
JSON, always HTTP 200, outcome in code
Retention
Images and results deleted after 90 days
Spec
openapi.yaml · result.schema.json · llms.txt
Coming from the old docs?

The testMode parameter has been removed, PDF upload is not supported, and region is now the most important optional parameter. See the changelog.

Quickstart

The program on the right is the complete flow: upload, poll, print. Pick your language, set TABSCANNER_API_KEY in the environment, 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 establishment and the total.

Every sample on this page is a real file from the examples folder of the docs repository and is exercised by an automated test suite against a mock of this API, so it runs as shown. Samples read the base URL from TABSCANNER_API_URL only so the tests can point them at the mock; you can delete that line.

What the sample does

It posts the image with region=us, checks that the body code is 200 or 300, then calls the result endpoint every second until the code is 202. A code of 301 means "not ready yet". Any other code is an error and the program stops with the message from the API.

Change region to the two-letter country code of your receipts. It is optional but it 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 jq. Usage: TABSCANNER_API_KEY=your-key ./scan.sh receipt.jpg
set -euo pipefail
API_URL="${TABSCANNER_API_URL:-https://api.tabscanner.com}"
POLL_SECONDS="${TABSCANNER_POLL_SECONDS:-1}"
IMAGE="${1:-receipt.jpg}"

# 1. Upload. The HTTP status is always 200; the outcome is the "code" field in the body.
upload=$(curl --silent --show-error --request POST "$API_URL/api/2/process" \
  --header "apikey: $TABSCANNER_API_KEY" --form "file=@$IMAGE" --form "region=us")
code=$(jq -r '.code' <<<"$upload")
if [[ "$code" != "200" && "$code" != "300" ]]; then
  echo "Upload failed: $code $(jq -r '.message' <<<"$upload")" >&2
  exit 1
fi
token=$(jq -r '.token' <<<"$upload")

# 2. Poll. 301 means not ready yet, 202 means done, anything else is an error.
for _ in $(seq 1 60); do
  sleep "$POLL_SECONDS"
  body=$(curl --silent --show-error "$API_URL/api/result/$token" --header "apikey: $TABSCANNER_API_KEY")
  code=$(jq -r '.code' <<<"$body")
  if [[ "$code" == "202" ]]; then
    jq -r '"Establishment: \(.result.establishment)\nTotal: \(.result.total) \(.result.currency)"' <<<"$body"
    exit 0
  elif [[ "$code" != "301" ]]; then
    echo "Result failed: $code $(jq -r '.message' <<<"$body")" >&2
    exit 1
  fi
done
echo "Timed out waiting for the result" >&2
exit 1

Authentication

Send your API key in the apikey request header on every call. There is no other authentication scheme, no OAuth flow and no query-string alternative.

If the header is missing or the key is unknown, the body carries code 400 with the message "API key not found". If the account has no credit left, the code is 401.

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 keys without downtime: create a new key in the dashboard, deploy it, then revoke the old one.
Request header
apikey: YOUR-API-KEY-FROM-THE-DASHBOARD

Missing or unknown key

Response body
{
  "message": "API key not found",
  "status": "failed",
  "status_code": 400,
  "success": false,
  "code": 400
}

Responses and codes

Every endpoint returns JSON with HTTP status 200, whatever happened. The outcome is the numeric code field in the body, so client code must read the body rather than the status line. Client libraries that raise on non-2xx status will never raise; libraries that check response.ok will always see true.

Body codes reuse HTTP numbers with different meanings

301 means the result is not ready yet, not a redirect. 402 means the token was not found, not payment required. 202 means the result is available. Always compare against the table on the right.

The envelope

These fields are present on every JSON response. The credit endpoint is the one exception: it returns a bare number on success.

successboolean

Whether the call succeeded. false for pending results and all errors.

statusstring

Human-readable outcome. success or failed.

One of: success, failed

status_codeinteger

Numeric code of the outcome. Same value as code.

codeinteger

Numeric outcome code. See the code table (x-tabscanner-codes). This, not the HTTP status, tells you what happened.

messagestring

Human-readable description of code.

Handling codes

  • 200 and 300 from process: the upload was accepted, use token. 300 additionally warns that the image is smaller than the recommended 720 by 1280 pixels.
  • 301 from result: keep polling.
  • 202 from result: done, read result.
  • 4xx: a problem with the request or the account. Fix it before retrying; a retry with the same input gives the same answer.
  • 5xx: a problem on the Tabscanner side. Retry with exponential backoff. For 500 (OCR failure) try a clearer image.
CodeMeaningWhat to do
200Process request submitted successfullyStore the returned token and start polling the result endpoint after about 2 seconds.
202Result availableRead the result object. Stop polling.
300Image uploaded, but did not meet the recommended dimension of 720x1280 (WxH)Processing continues. Accuracy may be reduced; capture higher resolution images if possible.
301Result not yet availableKeep polling, about once per second. Give up after 60 seconds and report a timeout.
400API key not foundCheck the apikey header. Do not retry with the same key.
401Not enough creditTop up credit or upgrade the plan. Do not retry until credit is available.
402Token not foundThe token is unknown or older than the retention period. Do not retry.
403No file detectedSend exactly one multipart part named file.
404Multiple files detected, can only upload 1 file per API callSend one file per call.
405Unsupported mimetypeSend image/jpeg or image/png.
406Form parser errorThe multipart body is malformed. Check the request encoding.
407Unsupported file extensionUse a .jpg, .jpeg or .png filename in the multipart part.
408File system errorRetry once. If it persists contact support.
500OCR FailureThe image could not be processed. Retry with a clearer image.
510Server errorRetry with exponential backoff.
520Database Connection ErrorRetry with exponential backoff.
521Database Query ErrorRetry with exponential backoff.

Upload a receipt

POSThttps://api.tabscanner.com/api/2/process

Submits one image as multipart/form-data. The image is queued and the call returns at once with a token. Use the token with the result endpoint.

The version number in the path selects the API version. The current version is 2; see Versioning.

Form fields

filefilerequired

The receipt image. JPG or PNG. Exactly one file per request. Recommended minimum 720x1280 pixels.

regionstringoptional

Two-letter ISO 3166-1 country code of the receipt's country, lower case. Selects number and date formats, language and regional extraction rules, and improves accuracy. 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

documentTypestringoptional

Type of document in the image. auto lets Tabscanner detect it and report it in result.documentType.

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

decimalPlacesintegeroptional

Hint for the number of decimal places used for amounts on the receipt (for example 3 for KWD or BHD). Improves number extraction when known in advance. This is a parsing hint, not an output format.

Range: 0 to 3

centsbooleanoptional

Treat amounts printed without a decimal separator as minor units. Only applies together with decimalPlaces=3 (for example 245 becomes 0.245, 1.574 stays 1.574).

Default: false

defaultDateParsingstringoptional

How to read an ambiguous numeric date such as 02/03/2019. d/m reads it as 2 March; m/d as 3 February.

One of: d/m, m/d

Response fields

In addition to the envelope:

successboolean

Whether the call succeeded. false for pending results and all errors.

statusstring

Human-readable outcome. success or failed.

One of: success, failed

status_codeinteger

Numeric code of the outcome. Same value as code.

codeinteger

Numeric outcome code. See the code table (x-tabscanner-codes). This, not the HTTP status, tells you what happened.

messagestring

Human-readable description of code.

tokenstring

Token to pass to the result endpoint. Present when code is 200 or 300.

duplicateboolean

true when the same image bytes were uploaded before on this account.

duplicateTokenstringnullable

Token of the first upload of this image when duplicate is true, otherwise null.

Duplicates still return a token

When the same image bytes are uploaded again, duplicate is true and duplicateToken names the first upload. The new token is still valid and resolves to the same data. See Duplicates and retries.

examples/curl/process.sh
#!/usr/bin/env bash
# Upload a receipt image. Prints the JSON response, which contains the token.
# Usage: TABSCANNER_API_KEY=your-key ./process.sh receipt.jpg
set -euo pipefail
API_URL="${TABSCANNER_API_URL:-https://api.tabscanner.com}"
IMAGE="${1:-receipt.jpg}"

curl --silent --show-error \
  --request POST "$API_URL/api/2/process" \
  --header "apikey: $TABSCANNER_API_KEY" \
  --form "file=@$IMAGE" \
  --form "region=us"
echo

Accepted

Response body
{
  "message": "Process request submitted successfully",
  "status": "success",
  "status_code": 200,
  "success": true,
  "code": 200,
  "duplicate": false,
  "duplicateToken": null,
  "token": "8f3c1e2ab7d94d1c9f0e5b6a2c4d7e81"
}

Same image uploaded before

Response body
{
  "message": "Process request submitted successfully",
  "status": "success",
  "status_code": 200,
  "success": true,
  "code": 200,
  "duplicate": true,
  "duplicateToken": "8f3c1e2ab7d94d1c9f0e5b6a2c4d7e81",
  "token": "2d9a4f6c8e1b4a7d9c3e5f7a1b2c3d4e"
}

Fetch the result

GEThttps://api.tabscanner.com/api/result/{token}

Returns the extracted data for a token from a previous upload. The path has no version number: the version is implied by the process call that created the token.

Path parameter

tokenstringrequired

The token returned by the process endpoint.

Response fields

In addition to the envelope:

successboolean

Whether the call succeeded. false for pending results and all errors.

statusstring

Human-readable outcome. success or failed.

One of: success, failed

status_codeinteger

Numeric code of the outcome. Same value as code.

codeinteger

Numeric outcome code. See the code table (x-tabscanner-codes). This, not the HTTP status, tells you what happened.

messagestring

Human-readable description of code.

resultobject

Structured data extracted from the receipt. Scalar fields are null when not found. Amounts are decimal numbers in the receipt's currency. Confidence values range from 0 (no confidence) to 1 (certain).

States

  • 301 Result not yet available. result is absent. Poll again after about a second.
  • 202 Result available. result holds the Result object.
  • 402 Token not found. The token is wrong or older than 90 days.
  • 500 OCR failure. The image could not be read. Ask for a better photo.

Results do not change once available. You can call this endpoint as many times as you like within the retention period; it does not use credit.

examples/curl/result.sh
#!/usr/bin/env bash
# Fetch the result for a token. Code 301 in the body means not ready yet, 202 means done.
# Usage: TABSCANNER_API_KEY=your-key ./result.sh TOKEN
set -euo pipefail
API_URL="${TABSCANNER_API_URL:-https://api.tabscanner.com}"
TOKEN="${1:?usage: result.sh TOKEN}"

curl --silent --show-error \
  "$API_URL/api/result/$TOKEN" \
  --header "apikey: $TABSCANNER_API_KEY"
echo

Not ready yet

Response body
{
  "message": "Result not yet available",
  "status": "failed",
  "status_code": 301,
  "success": false,
  "code": 301
}

Available (full example in Worked example)

Response body
{
  "message": "Result available",
  "status": "success",
  "status_code": 202,
  "success": true,
  "code": 202,
  "result": { "...": "see Result object" }
}

Check remaining credit

GEThttps://api.tabscanner.com/api/credit

Returns the number of credits left on the account as a bare JSON number, for example 1487. On an authentication error the body is the usual envelope with code 400.

Use it to alert your team before credit runs out. A process call made with no credit returns code 401 and does not queue the image.

examples/curl/credit.sh
#!/usr/bin/env bash
# Print the credits remaining on the account (a bare number).
# Usage: TABSCANNER_API_KEY=your-key ./credit.sh
set -euo pipefail
API_URL="${TABSCANNER_API_URL:-https://api.tabscanner.com}"

curl --silent --show-error \
  "$API_URL/api/credit" \
  --header "apikey: $TABSCANNER_API_KEY"
echo

Response

Response body
1487

Result object

The result field of a successful result response. Scalar fields are null when nothing was found. Amounts are decimal numbers in the receipt's currency, not minor units. Every confidence is a number from 0 to 1; treat values at or above 0.9 as reliable and use the validated* booleans as a shortcut for the highest confidence tier. Unknown extra fields may appear as features are added; ignore fields you do not recognise.

Merchant

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.

addressstringnullable

Merchant address text exactly as extracted, not normalised.

addressNormobjectnullable

The merchant address split into components. Any component may be null.

phoneNumberstringnullable

Merchant phone number as printed, not normalised.

urlstringnullable

Website address printed on the receipt.

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.

Date and time

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

Amounts

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.

Payment and classification

paymentMethodstringnullable

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

barcodesarray of array of string

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

Detected document type when documentType=auto was sent. Receipt or Invoice.

customFieldsobject

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

Lines

lineItemsarray of object

Product lines found on the receipt, top to bottom.

summaryItemsarray of object

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

LineItem object

Each entry of lineItems and summaryItems. Product lines describe things that were bought. Summary lines are the totals block at the bottom of the receipt, classified by lineType so you can find, say, the tip line even when the language is unfamiliar.

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.

unitnumbernullable

Unit measure amount when printed, for example weight or volume.

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.

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.

A product line

lineItems[1]
{
  "desc": "2 x SPARKLING WATER 500ML 3.50",
  "descClean": "SPARKLING WATER 500ML",
  "lineTotal": 7.00,
  "price": 3.50,
  "qty": 2,
  "unit": null,
  "productCode": null,
  "symbols": ["B"],
  "supplementaryLineItems": null,
  "lineType": "Product"
}

A summary line

summaryItems[1]
{
  "desc": "SALES TAX 8% 1.48",
  "descClean": "SALES TAX 8%",
  "lineTotal": 1.48,
  "price": null,
  "qty": 0,
  "unit": null,
  "productCode": null,
  "symbols": [],
  "supplementaryLineItems": null,
  "lineType": "Tax"
}

Worked example

A grocery receipt from a US deli: three products, sales tax, paid in cash. The full response is on the right. Reading it top to bottom:

  • establishment is the merchant name. validatedEstablishment is true because the phone number and address on the receipt matched a known business.
  • date and dateISO carry the same moment in two formats. The receipt printed a time, so it is not midnight.
  • total 19.93 equals subTotal 18.45 plus tax 1.48. validatedTotal is true, so you can trust it without checking the arithmetic yourself.
  • taxes lists each tax amount separately; here there is one. tip and serviceCharges are empty because nothing was printed.
  • cash and change come from the payment block, and paymentMethod is CASH.
  • lineItems has the three products. The banana line was sold by weight: unit is the weight, price the per-kilo rate and qty is 0 because no count was printed.
  • summaryItems repeats the totals block as lines, each tagged with lineType.

This example is the fixture used by the test suite; the numbers are consistent by construction. Real receipts are messier, which is what the confidence fields are for.

Response body
{
  "message": "Result available",
  "status": "success",
  "status_code": 202,
  "success": true,
  "code": 202,
  "result": {
    "establishment": "Corner Deli & Grocery",
    "establishmentConfidence": 0.97,
    "validatedEstablishment": true,
    "date": "2026-03-14 12:41:00",
    "dateISO": "2026-03-14T12:41:00",
    "dateConfidence": 0.99,
    "total": 19.93,
    "totalConfidence": 0.995,
    "validatedTotal": true,
    "subTotal": 18.45,
    "subTotalConfidence": 0.99,
    "validatedSubTotal": true,
    "tax": 1.48,
    "taxes": [
      1.48
    ],
    "taxesConfidence": [
      0.98
    ],
    "tip": null,
    "tipConfidence": null,
    "serviceCharges": [],
    "serviceChargeConfidences": [],
    "discount": null,
    "discounts": [],
    "discountConfidences": [],
    "rounding": null,
    "roundingConfidence": null,
    "cash": 20,
    "cashConfidence": 0.97,
    "change": 0.07,
    "changeConfidence": 0.97,
    "currency": "USD",
    "paymentMethod": "CASH",
    "address": "142 Elm Street, Springfield, IL 62701",
    "addressNorm": {
      "building": null,
      "number": "142",
      "street": "Elm Street",
      "suburb": null,
      "city": "Springfield",
      "state": "IL",
      "postcode": "62701",
      "country": "US"
    },
    "phoneNumber": "(217) 555-0142",
    "url": null,
    "barcodes": [],
    "expenseType": "Meals/Individual Meals while Traveling",
    "documentType": "Receipt",
    "customFields": {},
    "lineItems": [
      {
        "desc": "TURKEY CLUB SANDWICH 8.95",
        "descClean": "TURKEY CLUB SANDWICH",
        "lineTotal": 8.95,
        "price": 8.95,
        "qty": 1,
        "unit": null,
        "productCode": null,
        "symbols": [
          "A"
        ],
        "supplementaryLineItems": null,
        "lineType": "Product"
      },
      {
        "desc": "2 x SPARKLING WATER 500ML 3.50",
        "descClean": "SPARKLING WATER 500ML",
        "lineTotal": 7,
        "price": 3.5,
        "qty": 2,
        "unit": null,
        "productCode": null,
        "symbols": [
          "B"
        ],
        "supplementaryLineItems": null,
        "lineType": "Product"
      },
      {
        "desc": "BANANA 0.612 kg 2.50",
        "descClean": "BANANA",
        "lineTotal": 2.5,
        "price": 4.08,
        "qty": 0,
        "unit": 0.612,
        "productCode": "4011",
        "symbols": [],
        "supplementaryLineItems": null,
        "lineType": "Product"
      }
    ],
    "summaryItems": [
      {
        "desc": "SUB TOTAL 18.45",
        "descClean": "SUB TOTAL",
        "lineTotal": 18.45,
        "price": null,
        "qty": 0,
        "unit": null,
        "productCode": null,
        "symbols": [],
        "supplementaryLineItems": null,
        "lineType": "SubTotal"
      },
      {
        "desc": "SALES TAX 8% 1.48",
        "descClean": "SALES TAX 8%",
        "lineTotal": 1.48,
        "price": null,
        "qty": 0,
        "unit": null,
        "productCode": null,
        "symbols": [],
        "supplementaryLineItems": null,
        "lineType": "Tax"
      },
      {
        "desc": "TOTAL 19.93",
        "descClean": "TOTAL",
        "lineTotal": 19.93,
        "price": null,
        "qty": 0,
        "unit": null,
        "productCode": null,
        "symbols": [],
        "supplementaryLineItems": null,
        "lineType": "Total"
      },
      {
        "desc": "CASH 20.00",
        "descClean": "CASH",
        "lineTotal": 20,
        "price": null,
        "qty": 0,
        "unit": null,
        "productCode": null,
        "symbols": [],
        "supplementaryLineItems": null,
        "lineType": "Cash"
      },
      {
        "desc": "CHANGE 0.07",
        "descClean": "CHANGE",
        "lineTotal": 0.07,
        "price": null,
        "qty": 0,
        "unit": null,
        "productCode": null,
        "symbols": [],
        "supplementaryLineItems": null,
        "lineType": "Change"
      }
    ]
  }
}

Polling and timeouts

Processing is asynchronous. A sensible client:

  1. Uploads and stores the token immediately, so a crash after this point cannot lose the credit that was spent.
  2. Waits one to two seconds before the first poll. Most receipts are ready within a few seconds.
  3. Polls once per second while the code is 301. Faster polling does not speed up processing.
  4. Stops on 202 (done) or any code other than 301 (error).
  5. Gives up after about 60 seconds and reports a timeout. The token stays valid, so a later poll can still collect the result.

If you process many receipts, upload them all first and then poll each token in turn, rather than uploading and polling one at a time. Uploads are cheap; waiting is not.

Results are immutable once available and remain readable for 90 days, so it is safe to fetch a result more than once, for example from a retry queue or a second worker.

Timeline of a typical call

t=0.0s POST process → code 200, token
t=1.5s GET result → code 301
t=2.5s GET result → code 301
t=3.5s GET result → code 202, 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 models to apply 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

If your receipts come from a country that is not listed, contact support@tabscanner.com. Regional configurations are added on request, and enterprise accounts can have custom configurations for specific merchants and formats.

The currency field in the result is detected from the receipt itself and is independent of region.

curl
curl --request POST https://api.tabscanner.com/api/2/process \
  --header "apikey: $TABSCANNER_API_KEY" \
  --form "file=@receipt.jpg" \
  --form "region=gb" \
  --form "defaultDateParsing=d/m"

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; the defaultDateParsing field lets you fix 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; the time is the local time printed on the receipt.

Decimal places

Most currencies print two decimal places. Some, such as the Kuwaiti dinar 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 without a separator, so that 245 is read as 0.245 while 1.574 stays 1.574.

Cash rounding

Countries that have withdrawn small coins round cash totals 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.

PrintedFieldsRead as
02/03/2026region=us3 February 2026
02/03/2026region=gb2 March 2026
02/03/2026defaultDateParsing=m/d3 February 2026
1.574decimalPlaces=31.574
245decimalPlaces=3 cents=true0.245

Line items

Receipts rarely print one product per line. Descriptions wrap, quantities and unit prices sit on a 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 OCR read it.
  • descClean merges the description across adjacent lines and strips prices, quantities and discount text. Use this for display and matching.
  • qty and price are filled when the receipt prints them. A missing quantity is reported as 0, not assumed to be 1, so that you can tell "printed 1" from "not printed".
  • unit holds weights and volumes for items sold by measure.
  • symbols keeps the tax-class letters printed after the amount, useful for VAT reporting.
  • supplementaryLineItems appears only when text near the line could not be attached to it with confidence. Treat it as raw context.

Summary items

summaryItems lists the non-product lines from the totals block, each with a lineType such as SubTotal, Tax, Total, Cash, Change, Tip or ServiceCharge. The top-level amount fields 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 sum of lineTotal over lineItems should match subTotal, and subTotal plus taxes, tip and service charges minus discounts should match total. When they do not, a line was probably missed or merged; the confidence fields tell you which side to trust. Tabscanner's custom training service can tune line resolution for merchant formats you see often.

Receipt lines
2 x SPARKLING WATER 500ML
   @ 3.50                    7.00 B
BANANA
   0.612 kg @ 4.08/kg        2.50
Becomes
[
  { "descClean": "SPARKLING WATER 500ML", "qty": 2, "price": 3.50, "lineTotal": 7.00, "symbols": ["B"] },
  { "descClean": "BANANA", "qty": 0, "unit": 0.612, "price": 4.08, "lineTotal": 2.50 }
]

Duplicates and retries

Tabscanner fingerprints every upload. When the same image bytes arrive again on the same account, the response has duplicate: true and duplicateToken set to the token of the first upload. The new token is also valid and returns the same result.

Use this to make uploads idempotent: if your process crashed after uploading but before saving the token, upload the same file again and read duplicateToken to recover the original.

Retry policy

  • Network errors and 5xx codes: retry the same request with exponential backoff (for example 1, 2, 4, 8 seconds, then give up).
  • 301 on result: not an error, keep polling once per second.
  • 4xx codes: do not retry unchanged. Fix the request (400, 403 to 407), top up credit (401) or accept that the token is gone (402).
  • 500 OCR failure: the image itself is the problem; retrying the same bytes will fail the same way. Request a new photo.
Response body
{
  "message": "Process request submitted successfully",
  "status": "success",
  "status_code": 200,
  "success": true,
  "code": 200,
  "duplicate": true,
  "duplicateToken": "8f3c1e2ab7d94d1c9f0e5b6a2c4d7e81",
  "token": "2d9a4f6c8e1b4a7d9c3e5f7a1b2c3d4e"
}

Custom fields

Beyond the standard result, Tabscanner can extract fields that are specific to your use case and return them in customFields. Examples already in production: receipt number, merchant ID, VAT number, document number, loyalty card number, country, and the last four digits of the payment card.

Custom fields, merchant-specific configurations and custom training are part of the Enterprise and Pro Service plans. They are configured by the Tabscanner team for your account, so customFields is empty until something has been set up. Contact support@tabscanner.com with a handful of sample receipts and the fields you need.

customFields (example account)
"customFields": {
  "ReceiptNumber": "0042-118276",
  "VATNumber": "GB 123 4567 89",
  "CardLast4Digits": "4242",
  "Country": "GB"
}

Image guidance

Accuracy depends on the photo more than anything else. The OCR 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:

  • Resolution of at least 720 by 1280 pixels; modern phone cameras exceed this by default. Do not downscale before upload.
  • The whole receipt in frame, including the totals block at the bottom and the header at the top.
  • Flat and still. Put the receipt on a flat surface, hold the phone steady, and let the camera focus before shooting.
  • 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.

Long receipts should be photographed as a single tall image rather than in parts; the API accepts one image per call.

If you control the capture experience, a guided camera overlay that shows the receipt edges and blocks the shutter until 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 performance

Formats
JPEG and PNG. One file per call.
Image size
720 by 1280 pixels or larger recommended. Smaller images are processed with code 300 as a warning.
Processing time
Usually a few seconds; allow up to 60 seconds before treating a result as timed out.
Credits
One credit per accepted upload (codes 200 and 300). Result and credit calls are free. Rejected uploads (4xx) do not use credit.
Retention
Images and results are deleted 90 days after upload.
Rate limits
Depend on your plan. Contact support before sending sustained bursts of more than a few requests per second.

Large batches: upload in parallel (a handful of concurrent connections is plenty), record every token, then poll. Do not open one connection per receipt and hold it while waiting.

Plans

Starter (free monthly credits), Per Credit, Business, Enterprise and Pro Service plans are described at tabscanner.com. Credits and limits are per account, not per API key.

Security and data handling

  • Transport. HTTPS only. Requests over plain HTTP are not served.
  • At rest. Uploaded images and extracted results are encrypted at rest.
  • Retention. Images and results are deleted 90 days after upload. Persist anything you need in your own systems.
  • Keys. API keys are shown once in the dashboard and can be rotated at any time. Multiple keys per account are supported.
  • 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 support@tabscanner.com.

Versioning

The API version is part of the process path: /api/2/process. The result endpoint has no version because a token already belongs to the version that created it. The current version is 2.

Within a version, changes are additive: new optional form fields and new result fields may appear at any time, which is why clients should ignore unknown fields. Field removals, type changes and behavioural changes only happen in a new version number, and the previous version keeps working for existing integrations.

Recent changes within version 2 are listed in the changelog.

AI agents and LLMs

If you are wiring Tabscanner into an agent, or you are an agent reading this, the short version is: one tool called scan_receipt that uploads, polls and returns the result object, plus get_credit. Do not expose the raw polling to the model.

The AI agents page has copy-pasteable tool definitions for Claude and OpenAI-style function calling, a reference implementation, the rules an agent must follow (body code, not HTTP status; stop conditions; idempotency), and which result fields to trust first.

Machine-readable resources:

Support

Email support@tabscanner.com for technical questions, region requests, custom fields and anything not answered here. Include the token of a problematic upload and, if you can, the image, so we can reproduce it.

Found a mistake in these docs or a sample that does not run? The documentation, samples and tests live together in the docs repository, and every sample is run against a mock of the API on each change.