# Arabic Receipt Reader (OCR + Saudi ZATCA QR) (`saif-tools/arabic-receipt-reader`) Actor

Reads Arabic and English receipts and invoices, including Arabic-Indic digits: full OCR, total/VAT/subtotal/line-item candidates checked against each other, a needsReview flag, and exact fields from Saudi ZATCA e-invoice QR codes (signature not checked). For dense documents use Arabic Image OCR.

- **URL**: https://apify.com/saif-tools/arabic-receipt-reader.md
- **Developed by:** [saif alnuaimi](https://apify.com/saif-tools) (community)
- **Categories:** AI, Developer tools, Agents
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

Pay per event

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## Arabic Receipt Reader (OCR + Saudi ZATCA QR)

Read Arabic and English receipts and invoices, including Arabic-Indic digits (٠١٢٣٤٥٦٧٨٩) and Persian digits (۰-۹). For every image you get:

- the full text and lines, each with a confidence score, in right-to-left reading order;
- **candidates** for the total, subtotal, VAT, VAT number, date and line items, each with its confidence;
- a **reconciliation** check: do the line items + VAT add up to the total, does subtotal + VAT equal the total, does the total match the e-invoice QR?
- a `needsReview` flag that tells you which receipts a person should check, with `reviewReasons`;
- the **exact fields from a Saudi ZATCA e-invoice QR** (seller, VAT number, timestamp, total, VAT), decoded from the QR, never guessed from the OCR text.

> **Dense documents, article screenshots or chat screenshots?** Use **[Arabic Image OCR](https://apify.com/saif-tools/arabic-image-ocr)** (`arabic-image-ocr`) instead. Its plain-text mode is faster, cheaper and reads dense small print better. It also has a QR-only mode.

> This tool reads and reports the fields on an invoice. It doesn't issue, send or receive e-invoices, isn't an accredited service provider, and doesn't confirm compliance with ZATCA or UAE rules.

### How it reads

A neural OCR engine (EasyOCR, Arabic + English) reads the image, and every number is cross-checked by a dedicated Arabic-Indic digit classifier. When the two read a number differently, you get the more confident reading plus `alternativeReading`. Every image is downscaled to at most 4 megapixels before OCR; `imageSize` shows the original and processed sizes, and boxes are in original-image pixels.

### The `receipt` object

- `totalCandidate`, `subtotalCandidate`, `vatCandidate`: the value, the text as printed (`textRaw`), the line it came from, a `confidence` from 0 to 100 and `lowConfidence` (below `confidenceThreshold`, default 80).
- `vatNumberCandidate` and `dateCandidate`.
- `lineItems`: the description and amount of each line, with their confidence.
- `sumCheck`: whether the line items add up to the subtotal, the total minus VAT, or the total, within `sumToleranceCents` (default 0.05).
- `vatCheck`: whether subtotal + VAT equals the total.
- `qrCrossCheck`: whether the printed total and VAT agree with the invoice QR (tags 4 and 5), and the printed VAT number with tag 2: `agree`, `mismatch`, `not_in_qr` or `not_found_in_text`.
- `reconciliation`: `{"reconciled": true/false, "by": [...]}`, with `by` naming what matched (`lineItemsPlusVatEqualTotal`, `lineItemsEqualTotal`, `lineItemsEqualSubtotalAndSubtotalPlusVatEqualsTotal`, `subtotalPlusVatEqualsTotal`, `totalMatchesQr`).
- `needsReview`, `reviewReasons` and `notes`.

#### When is `needsReview` true?

| `reviewReasons` entry | Meaning |
|---|---|
| `line_items_do_not_add_up` | The line items don't add up to the subtotal / total (within the tolerance) |
| `subtotal_plus_vat_differs_from_total` | Subtotal + VAT is not the total |
| `printed_value_differs_from_qr: total` / `vat` | The printed value disagrees with the e-invoice QR |
| `no_total_found` | No total line was recognised |
| `total_low_confidence_and_nothing_to_reconcile` | There are no line items, VAT or QR total to check the total against, and its confidence is below `confidenceThreshold` |

**Low OCR confidence alone does not flag a receipt whose numbers reconcile.** When the items + VAT add up to the total (or the total matches the QR), a low confidence score is only mentioned in `notes`. On our test set this cut the flag rate on correctly read receipts from 83% to 21%, while still flagging every misread or inconsistent receipt (see `test-results/REPORT.md`).

**Totals are candidates, not facts.** OCR can misread a digit, a decimal separator or which line is the total. Numbers that reconcile are much more likely to be right, but not guaranteed. Don't book, pay or file anything on OCR values alone. When an invoice has a ZATCA QR, `exactQrFields.invoiceTotal` is the value encoded by the seller's system and is the better source.

### Exact QR fields

When an image contains a Saudi e-invoice QR (ZATCA TLV, base64), you get `exactQrFields`:

| Tag | Field |
|---|---|
| 1 | `sellerName` |
| 2 | `vatNumber` (plus `vatNumberFormatValid`: 15 digits, starts and ends with 3; a flag, never a rejection) |
| 3 | `timestamp` (plus `timestampIso`) |
| 4 | `invoiceTotal` (with VAT) |
| 5 | `vatTotal` |
| 6–9 | reported as present, with their lengths (hash, signature, public key, certificate signature) |

`tagsFound` lists the tags in the QR. `qrFormat` is informational: `phase1`, `phase2-standard`, `phase2-simplified` or `other`.

**We decode the QR TLV but do NOT verify its cryptographic signature/stamp** (`signatureChecked` is always `false`). A QR that decodes cleanly can still be forged or belong to another invoice. These are the exact fields stored in the QR, not proof that the invoice is genuine or registered.

`qr.status` is `zatca_tlv`, `not_zatca_tlv` (another kind of QR; its text is in `qr.codes[].rawText`) or `no_qr_found`.

### Input

| Field | Default | Notes |
|---|---|---|
| `imageUrls` | | Public image links (JPEG, PNG, WebP, GIF, BMP, TIFF) |
| `keyValueStoreRecords` | | Uploaded images: `record-key` for this run's store, or `store-name-or-id/record-key` |
| `includeQrFields` | `true` | Decode a ZATCA QR and cross-check against it (included in the price, no extra charge) |
| `confidenceThreshold` | 80 | The `lowConfidence` threshold |
| `sumToleranceCents` | 5 | Reconciliation tolerance, in 1/100 of the currency |
| `deskew`, `digitSecondPass`, `maskQrCodes` | `true` | Image options |
| `outputLines`, `outputWords`, `stripTatweel` | `true`, `false`, `true` | Output detail |
| `maxImageSizeMb` | 20 | Limit per image |

**No images in the input?** The actor reads a small bundled sample receipt so you can see the output (`sample: true`, free). This is also what the input form's default run does. If the input links to an example image that can't be downloaded, the sample is read instead. Either way the run's status message and `OUTPUT.note` say so (for example "Input image failed to download; returned built-in sample instead").

```json
{
  "imageUrls": [{"url": "https://example.com/receipt-1.jpg"}, {"url": "https://example.com/receipt-2.png"}],
  "confidenceThreshold": 85
}
```

**Memory:** the OCR models need about 1.5–1.8 GB of RAM, so the actor runs at **4 GB** (minimum and default 4096 MB) and reads one image at a time. If it is started with less than about 3 GB, it stops straight away with the message "increase memory to 4 GB" and charges nothing. A receipt screenshot takes a few seconds; a phone photo can take up to about a minute.

### Output

A shortened row for a phase-2 receipt:

```json
{
  "source": "https://example.com/receipt.png",
  "sample": false,
  "status": "ok",
  "mode": "full",
  "text": "Al Waha Trading Co.\nSimplified Tax Invoice\nLaptop stand 1000.00\nVAT 15% 150.00\nTotal 1150.00",
  "imageSize": {"original": [760, 747], "processed": [760, 747], "downscaledBy": 1.0, "maxProcessedMegapixels": 4.0},
  "qr": {"status": "zatca_tlv", "count": 1},
  "exactQrFields": {"sellerName": "Al Waha Trading Co. شركة الواحة", "vatNumber": "300000000000003", "vatNumberFormatValid": true,
                    "timestamp": "2026-09-30T08:05:44", "invoiceTotal": "1150.00", "vatTotal": "150.00",
                    "qrFormat": "phase2-simplified", "tagsFound": [1, 2, 3, 4, 5, 6, 7, 8, 9], "signatureChecked": false},
  "receipt": {
    "totalCandidate": {"value": 1150.0, "textRaw": "1150.00", "confidence": 61.2, "lowConfidence": true},
    "vatCandidate": {"value": 150.0, "confidence": 90.3, "lowConfidence": false},
    "lineItems": [{"description": "Laptop stand", "value": 1000.0, "confidence": 72.5, "lowConfidence": true}],
    "sumCheck": {"itemsSum": 1000.0, "comparedTo": "totalCandidate minus vatCandidate", "difference": 0.0, "tolerance": 0.05, "addsUp": true},
    "qrCrossCheck": {"total": "agree", "vat": "agree", "vatNumber": "agree"},
    "reconciliation": {"reconciled": true, "by": ["lineItemsPlusVatEqualTotal", "totalMatchesQr"]},
    "needsReview": false,
    "reviewReasons": [],
    "notes": ["low OCR confidence on totalCandidate, lineItems, but the numbers reconcile, so not flagged"]
  },
  "chargedEvents": ["full-ocr-screenshot"]
}
```

Images that can't be read get a row with `status: "error"` and an `error` message. **Error rows are free.** The `OUTPUT` record summarises the run: images processed, failed and skipped, rows needing review, sample rows, the events charged and how long it took.

### Pricing (pay per event)

| Event | When | Price |
|---|---|---|
| `full-ocr-screenshot` | Each image whose original is up to 4 megapixels (screenshots, scans, most receipts) | $0.025 |
| `full-ocr-photo` | Each image whose original is over 4 megapixels (phone photos) | $0.05 |

- **Photo or screenshot is decided by the original image's pixel count, before the 4 MP downscale.** A 12 MP phone photo is billed as `full-ocr-photo` even though it is read at 4 MP.
- **The Saudi e-invoice QR fields and the QR cross-check are included in the price.** A receipt with a ZATCA QR costs the same as one without: $0.025 or $0.05.
- Error rows and sample rows (`sample: true`) are free.
- `chargedEvents` in each row lists only the events actually charged for that row.
- **Spending limit:** before each image the actor checks that its price still fits under your run's maximum cost. If it doesn't, the actor stops cleanly and keeps every row saved so far (`OUTPUT.stoppedByChargeLimit`, `skippedSources`). You are never charged for a row that wasn't saved.

### Limitations

- **OCR is not perfect.** On our synthetic receipts this actor read about 90% of Arabic-Indic digits correctly. Real photos (blur, glare, crumpled or faded thermal paper) score lower. Use `needsReview`.
- Very dense pages (a full article screenshot) are slow and read worse than with [Arabic Image OCR](https://apify.com/saif-tools/arabic-image-ocr).
- Handwriting, decorative calligraphy and heavy diacritics are read poorly.
- Decimal separators: `٫`, `.` and `,` are all used on Gulf receipts. Something like `٢٢,٥٠` is read as 22.50 and marked `separatorAmbiguous`.
- Field detection relies on keywords in Arabic and English (الإجمالي، المجموع، ضريبة القيمة المضافة, Total, VAT, Subtotal…). Unusual layouts may give no `totalCandidate` (`no_total_found`).
- Only Saudi ZATCA QR codes are decoded into fields.
- Only the first frame of multi-page TIFF and GIF files is read.

### Privacy

Images are downloaded into the run's own container, processed in memory and not sent to any third-party service. All OCR runs inside the actor. Results are stored only in your run's dataset and key-value store, under your Apify account and its retention settings. Receipts and invoices can contain personal data (names, VAT numbers, sometimes card digits), so make sure you have the right to process the images you submit, and delete runs you no longer need.

# Changelog

This Actor's version history is a separate document: https://apify.com/saif-tools/arabic-receipt-reader/changelog.md

# Actor input Schema

## `imageUrls` (type: `array`):

Public links to receipt/invoice images (JPEG, PNG, WebP, GIF, BMP or TIFF). Images over 4 megapixels are billed as photos and downscaled to 4 MP before OCR. Leave empty to run the bundled sample receipt (free, marked sample: true).

## `keyValueStoreRecords` (type: `array`):

Images already uploaded to an Apify key-value store. Write <code>record-key</code> for this run's default store, or <code>store-name-or-id/record-key</code>.

## `includeQrFields` (type: `boolean`):

Decode a Saudi ZATCA e-invoice QR when present and cross-check the printed total and VAT against it. Included in the receipt price: no extra charge. Turn off only if you do not want QR fields in the output.

## `confidenceThreshold` (type: `integer`):

OCR confidence (0-100) below which a number is marked lowConfidence. Low confidence only sets needsReview when the total cannot be reconciled with line items, VAT or the QR total.

## `sumToleranceCents` (type: `integer`):

How far line items + VAT (or subtotal + VAT, or the QR total) may be from the total and still count as reconciled. 5 = 0.05 SAR/AED.

## `deskew` (type: `boolean`):

Detects and corrects rotation up to ±10°.

## `digitSecondPass` (type: `boolean`):

Re-reads Arabic-Indic and Persian digits with a dedicated digit classifier. In full-OCR mode it cross-checks each number.

## `maskQrCodes` (type: `boolean`):

Blanks detected QR codes before OCR so they don't turn into junk text.

## `stripTatweel` (type: `boolean`):

The normalised fields drop tatweel and map Arabic-Indic and Persian digits to 0-9 (٫ → . and ٬ → ,). The raw fields keep the text as read.

## `outputLines` (type: `boolean`):

One entry per text line with its confidence, bounding box and direction.

## `outputWords` (type: `boolean`):

Adds the words of each line with their bounding boxes. Makes rows much larger.

## `maxImageSizeMb` (type: `integer`):

Larger files are skipped with a free error row.

## Actor input object example

```json
{
  "includeQrFields": true,
  "confidenceThreshold": 80,
  "sumToleranceCents": 5,
  "deskew": true,
  "digitSecondPass": true,
  "maskQrCodes": true,
  "stripTatweel": true,
  "outputLines": true,
  "outputWords": false,
  "maxImageSizeMb": 20
}
```

# Actor output Schema

## `results` (type: `string`):

One row per image: text, lines with confidence, image sizes, receipt field candidates, needsReview, QR status and exact QR fields.

## `receipts` (type: `string`):

Total/subtotal/VAT/line-item candidates, reconciliation, needsReview and reviewReasons.

## `summary` (type: `string`):

Images processed, failed and skipped at the spending limit, and the events charged.

## `qr` (type: `string`):

Fields decoded from Saudi ZATCA e-invoice QR codes (signature not checked).

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {};

// Run the Actor and wait for it to finish
const run = await client.actor("saif-tools/arabic-receipt-reader").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {}

# Run the Actor and wait for it to finish
run = client.actor("saif-tools/arabic-receipt-reader").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{}' |
apify call saif-tools/arabic-receipt-reader --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,saif-tools/arabic-receipt-reader"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/3vr33Pb8HbP228mBC/builds/tdhbeB3RNI7CEpBY4/openapi.json
