# OCR Image to Text API: Words, Boxes, Confidence (`pradio/image-text`) Actor

Extract text from images with OCR: paste image URLs, send base64 files, or read key-value store records. Each image returns its full text, words, lines and paragraphs with bounding boxes, confidence scores and the detected language. Images that fail to load are free; one with no text is billed.

- **URL**: https://apify.com/pradio/image-text.md
- **Developed by:** [Pradio Actors](https://apify.com/pradio) (community)
- **Categories:** Developer tools, AI
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $2.80 / 1,000 image returneds

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

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

## OCR Image to Text API: Words, Boxes, Confidence

### What does OCR Image to Text API do?

OCR Image to Text API extracts the text from every image you send, one row per image, with each word, line and paragraph boxed and scored. Each row also carries the image's own pixel dimensions. Paste the URLs, or send the files themselves as base64 or from your key-value store, press Start, and each entry is read with OCR. An entry the run cannot read costs nothing, and its row says why.

The fill rate is measured, not claimed. Over 57 images this Actor had never seen during development, mostly posters, photographs, signs and scanned pages, 53 came back as billed rows. 47 read text, and 44 of those averaged 80 or more in confidence. The other 6 carry no text at all (a leather book binding, packaging pictograms, sunsets) and came back empty, which is the right answer. The last 4 read as marks that form no words in any language the run could detect, and came back as free `unreadable` rows. Each returned image costs $0.004, plus two $0.00005 start events per run at the default memory.

### Who uses OCR Image to Text API

| Buyer | What they run it for |
|---|---|
| Developers adding OCR to an app | Post a list of image URLs through the API and get JSON rows back, one per image |
| Operations and back-office teams | Turn scanned documents, receipts and screenshots into text they can search and process |
| Data and research teams | Pull the text out of image collections for indexing, cataloguing or analysis |
| AI-agent builders | Give an agent a way to read text inside images through the MCP server below |

### Features

- **A whole list of images in one run.** Paste as many entries as you need. Each readable image returns one data row with its text, layout, confidence scores and language.
- **Three ways to send images.** Public URLs in `image_urls`, files sent inside the input as base64 in `base64Images`, or files saved in one of your own key-value stores through `keyValueStoreId` and `keyValueStoreRecords`. Each row says which in `image_source` and names its image.
- **Detected language.** `detected_language` says what language the read text is actually in, as a three-letter language code (ISO 639), beside the `language` the OCR was asked to read in. A mismatch is the sign to re-run with the right language.
- **Optional English translation.** Set `translateTo` to `en` and text read in German, French, Spanish, Italian, Dutch, Polish, Russian or Chinese also comes back machine-translated into English in `translation`. The translation runs inside the Actor, so no outside service sees your text, and it costs nothing extra.
- **Output detail you choose.** `outputDetail` returns every word, line and paragraph (`full`, the default), lines and paragraphs only (`lines`), or the text alone (`text`) for smaller rows.
- **Word, line and paragraph detail.** Every piece of text carries its text, a confidence score and a bounding box with pixel coordinates. Tesseract scores and boxes each word on its own. PaddleOCR, the engine for English and Chinese, reads whole lines, so each word there carries its line's score and a box cut from the line's.
- **Image dimensions included.** Width and height are read from each file's own header on PNG, JPEG, GIF, BMP and WebP images.
- **Two engines, picked by language.** English and Chinese read with PaddleOCR, a modern deep-learning reader that holds up on photographs, signs and screenshots. Any other language you name reads with Tesseract and that language's own model. `ocr_engine` on every row says which one read it.
- **Language choice.** `eng` is the default, common two-letter codes are mapped for you, and codes joined with `+` read mixed-language images.
- **Direct fetch only.** A plain HTTPS GET with a declared user agent. No browser, no proxy, no login.
- **Named misses, never silent drops.** Every entry that cannot be read comes back as an uncharged status row with a named reason.
- **A cap you control.** `maxItems` bounds how many rows one run returns, and the run reports what it left out.
- **Run accounting.** Every run leaves a RUN\_SUMMARY entry in the key-value store: entries fetched, rows pushed, events charged, duplicates dropped.

### What you can count on

- You pay only for rows whose status is `ok`. An entry the run cannot answer is pushed as an uncharged ITEM\_STATUS row that names the entry and says why.
- Every row is charged only after it is written to your dataset. A row you cannot see is never billed.
- A run that finds nothing returns one NO\_TEXT\_FOUND row that says so, never an empty dataset.
- A spending limit stops the run cleanly with a STOPPED\_EARLY row saying how many rows were returned and how many were not.
- Every run writes a RUN\_SUMMARY entry with rowsFetched, rowsPushed, rowsCharged and duplicatesDropped, so a short run and a broken one are told apart.
- No value is invented. A field the image does not yield stays null rather than filled in.

### Why this one

- **One run reads the whole list.** The most-used alternative on this platform takes one image per run. Run on one of the images in the example below on 2026-09-22, it returned 1 row for that image. This Actor read the same image in one run over the whole six-URL list: 5 data rows and one named miss.
- **A lower price per image.** Each returned row costs $0.004 against the alternative's $0.00499, measured 2026-09-22.
- **A smaller start fee.** The most-used alternative bills a $0.002 start fee on every run. The platform's own start charge on this Actor is $0.00005 per GB of memory: two events, $0.0001, a run at the default 2048 MB.
- **Misses are free and explained.** An image the run cannot read pushes an uncharged row naming the miss instead of billing you for it. A read that forms no words in a detectable language at low confidence, typically an image in a script the chosen language does not read, comes back free as `unreadable`.
- **A confidence score per image.** `mean_confidence` averages the word scores. Over 57 unseen images, 47 read text and 44 of those scored 80 or more. The lower scores were old or stylised lettering. The 4 reads that formed no words, one of them an Arabic page read with English selected, came back free as `unreadable`. Name the text's language and the right engine reads it.
- **The source URL on every row.** Each row carries the `image_url` it was read from, so every answer stays traceable to the file you sent.

### What data does OCR Image to Text API return?

One row per image. This is a real row from a run of the example input below, a Simplified Chinese poem. It was read with `language` set to `en+zh-cn` and translated with `translateTo` set to `en`. The English images in the same run come back with `translation_note`: `already English`.

```json
{
  "status": "ok",
  "image_url": "https://tesseract.projectnaptha.com/img/chi_sim.png",
  "image_source": "url",
  "image_name": null,
  "full_text": "冬日平泉路晚归\n山路难行日易斜\n烟村霜树欲栖鸦\n夜归不到应闲事\n热饮三杯即是家",
  "paragraphs": [
    {
      "text": "冬日平泉路晚归\n山路难行日易斜\n烟村霜树欲栖鸦\n夜归不到应闲事\n热饮三杯即是家",
      "confidence": 99.5,
      "bbox": {
        "x0": 357,
        "y0": 43,
        "x1": 1111,
        "y1": 638
      }
    }
  ],
  "word_count": 5,
  "mean_confidence": 99.5,
  "image_width": 1486,
  "image_height": 668,
  "words": [
    {
      "text": "冬日平泉路晚归",
      "confidence": 99.7,
      "bbox": {
        "x0": 363,
        "y0": 43,
        "x1": 1102,
        "y1": 145
      }
    },
    {
      "text": "山路难行日易斜",
      "confidence": 99,
      "bbox": {
        "x0": 370,
        "y0": 167,
        "x1": 1108,
        "y1": 268
      }
    },
    {
      "text": "烟村霜树欲栖鸦",
      "confidence": 99.5,
      "bbox": {
        "x0": 357,
        "y0": 291,
        "x1": 1109,
        "y1": 393
      }
    },
    {
      "text": "夜归不到应闲事",
      "confidence": 99.7,
      "bbox": {
        "x0": 358,
        "y0": 410,
        "x1": 1111,
        "y1": 515
      }
    },
    {
      "text": "热饮三杯即是家",
      "confidence": 99.7,
      "bbox": {
        "x0": 361,
        "y0": 536,
        "x1": 1109,
        "y1": 638
      }
    }
  ],
  "language": "eng+chi_sim",
  "detected_language": "cmn",
  "translation": "The winter springs are late.\nThe mountains are hard to walk.\nThe smoke village frost trees are a raven.\nI can't get my time back.\nThree drinks is home.",
  "translation_language": "en",
  "translation_note": null,
  "lines": [
    {
      "text": "冬日平泉路晚归",
      "confidence": 99.7,
      "bbox": {
        "x0": 363,
        "y0": 43,
        "x1": 1102,
        "y1": 145
      }
    },
    {
      "text": "山路难行日易斜",
      "confidence": 99,
      "bbox": {
        "x0": 370,
        "y0": 167,
        "x1": 1108,
        "y1": 268
      }
    },
    {
      "text": "烟村霜树欲栖鸦",
      "confidence": 99.5,
      "bbox": {
        "x0": 357,
        "y0": 291,
        "x1": 1109,
        "y1": 393
      }
    },
    {
      "text": "夜归不到应闲事",
      "confidence": 99.7,
      "bbox": {
        "x0": 358,
        "y0": 410,
        "x1": 1111,
        "y1": 515
      }
    },
    {
      "text": "热饮三杯即是家",
      "confidence": 99.7,
      "bbox": {
        "x0": 361,
        "y0": 536,
        "x1": 1109,
        "y1": 638
      }
    }
  ],
  "ocr_engine": "paddleocr",
  "row_type": "ROW"
}
```

Every field, and where its value is read from:

| Field | Type | What it is |
|---|---|---|
| `image_url` | string | The image URL this row was read from: your input entry, echoed so every row names its source. Null on an image sent as base64 or read from a key-value store. |
| `image_source` | string | Where the image came from: `url`, `base64` or `key_value_store`. |
| `image_name` | string | The name of an image with no URL: the `name` you gave a `base64Images` entry, or `store/key` for a file read from a key-value store. Null on a URL entry. |
| `full_text` | string | The whole text the OCR read out of the image. An empty string means the image was readable but carried no text. |
| `paragraphs` | array | The paragraphs the OCR found. Each entry carries `text`, a `confidence` score and a `bbox` pixel box. Null when `outputDetail` is `text`. |
| `word_count` | number | How many words the OCR read, the length of `words`. Zero on a readable image with no text. |
| `mean_confidence` | number | The mean of the words' confidence scores, 0 to 100. A read that forms no words in a detectable language and scores under 70 comes back as a free `unreadable` miss. On a billed row a higher score is a cleaner read. Stylised lettering, a blurred photo or a language you did not name scores lower, so filter on it before you use `full_text`. Null when no words were read. |
| `image_width` | number | The image's width in pixels, sniffed from the file's own header. Null when the format's dimensions are not sniffed. |
| `image_height` | number | The image's height in pixels, sniffed the same way as `image_width`. |
| `words` | array | Every word the OCR found. Each entry carries `text`, a `confidence` score and a `bbox` pixel box. On `paddleocr` rows a word's score is its line's and its box is cut from the line's. Null when `outputDetail` is `lines` or `text`. |
| `language` | string | The language the OCR was asked to read this image in: your `language` input, with a two-letter code written out in full, so `en` reads `eng`. It is what you asked for, not what the text turned out to be; `detected_language` says that. |
| `detected_language` | string | The language the read text is written in, detected from the text itself as a three-letter language code from ISO 639 (`eng`, `deu`, `fra`, `spa`, `cmn` and so on). Null when the text has fewer than 20 letters or is too mixed to judge. |
| `ocr_engine` | string | Which engine read this image: `paddleocr` for English and Chinese, `tesseract` for every other language you name. |
| `lines` | array | The text lines the OCR found. Each entry carries `text`, a `confidence` score and a `bbox` pixel box. Null when `outputDetail` is `text`. |
| `translation` | string | The read text machine-translated into English, when `translateTo` is `en` and `detected_language` is one of the eight languages translated. Paragraphs are separated by a blank line. Null when translation is off or the text was not translated; `translation_note` says why. |
| `translation_language` | string | `en` when a translation was made, null otherwise. |
| `translation_note` | string | Why `translation` is null (`already English`, no language detected, or no translation model for the detected language, named by its code), or what was left out of a translation. Null when translation is off or the whole text was translated. |
| `row_type` | string | `ROW` on a data row. `ITEM_STATUS` on a per-entry miss. `NO_TEXT_FOUND` when the run has nothing to return. `STOPPED_EARLY` when the charge limit cut the run short. |
| `status` | string | The per-entry verdict: `ok` on a data row. On an ITEM\_STATUS row, the named miss: `not_found`, `fetch_failed`, `blocked`, `bad_url`, `bad_input`, `timeout` or `unreadable`. `unreadable` means the text read forms no words in any language at a confidence under 70. The image is likely in a script the chosen language does not read, and the text read stays on the row. |
| `reason` | string | Status rows only: the miss reason on ITEM\_STATUS, why nothing was returned on NO\_TEXT\_FOUND, or why the run stopped early. |
| `rowsFetched` | integer | Status rows only: how many entries the run read before de-duplication and the cap. |
| `rowsReturned` | integer | Status rows only: how many data rows are in the dataset. |
| `rowsRemaining` | integer | Status rows only: how many fetched rows were not returned. |

A miss looks like this, a real row from the same run. It is pushed so you can see the verdict, and it is never billed:

```json
{
  "image_url": "https://example.com/",
  "image_source": "url",
  "image_name": null,
  "status": "bad_url",
  "reason": "the URL did not return image bytes (text/html)",
  "full_text": null,
  "paragraphs": null,
  "word_count": null,
  "mean_confidence": null,
  "image_width": null,
  "image_height": null,
  "words": null,
  "language": null,
  "detected_language": null,
  "ocr_engine": null,
  "lines": null,
  "translation": null,
  "translation_language": null,
  "translation_note": null,
  "row_type": "ITEM_STATUS"
}
```

`image_name` is empty on every row of the example run above, because every entry there is a URL, and a URL row is named by `image_url`. It is filled on images sent through `base64Images` or read from a key-value store. Every other field carried a value on at least one row, and the Overview view in the Console shows the data fields as a table.

### How much does it cost?

Two prices cover everything:

- **$0.004 per returned image** (the `document-returned` event in the Console). One charged unit is one image's text read and returned as a row, billed only after the row is written to your dataset.
- **$0.00005 per `apify-actor-start` event.** The platform's per-run start charge: one event per GB of memory, minimum one. The default memory is 2048 MB, so a run bills two.

What that means at volume, where every image that is fetched and read is billed, whether it carries text or not, unless the read comes back `unreadable`:

| You send | Billed rows back | Image cost | Start event |
|---|---|---|---|
| 100 image URLs | about 93 | $0.37 | $0.0001 |
| 1,000 image URLs | about 930 | $3.72 | $0.0001 |
| 10,000 image URLs | about 9,298 | $37.19 | $0.0001 |

The billed rows are projected at the measured 93% fill. Free miss rows come back in the dataset beside them and are not counted in the table. In the scale run, 53 of 57 images were billed rows and 4 came back as free `unreadable` rows. Two scenarios: 100 image URLs at that fill cost about $0.37 plus two $0.00005 start events. 1,000 image URLs cost about $3.72 plus the same two start events. An image that misses costs nothing on either line.

What is never charged: ITEM\_STATUS miss rows, `unreadable` reads among them. The same goes for the NO\_TEXT\_FOUND row, the STOPPED\_EARLY row, duplicates dropped before pushing and entries beyond the `maxItems` cap.

To keep a run cheap: set a per-run spending limit in the Console and the run stops cleanly at it, and de-duplicate your list first. Repeats are dropped before billing anyway, but a shorter list finishes faster.

### How do I use OCR Image to Text API?

1. Open OCR Image to Text API on the Apify Store and press **Try for free**.
2. Paste your image URLs into **Image URLs**, one per line. The input comes prefilled with six public test images, so a first run works out of the box. Three are in English and one in Simplified Chinese. One is a logo with no text, and one is a web page that is not an image.
3. Set **Language** to the language your images are written in. The example reads English and Simplified Chinese (`en+zh-cn`) and turns on **Translate to** English. Set **Maximum items** to cap the run.
4. Press **Start**. Each entry is fetched and read, and rows land in the dataset as they finish.
5. Open the dataset to read the rows, and the key-value store's RUN\_SUMMARY entry for the run's counts.

Example input, the same one the sample rows above came from:

```json
{
  "image_urls": [
    "https://tesseract.projectnaptha.com/img/eng_bw.png",
    "https://raw.githubusercontent.com/naptha/tesseract.js/master/tests/assets/images/bill.png",
    "https://raw.githubusercontent.com/naptha/tesseract.js/master/tests/assets/images/cosmic.png",
    "https://tesseract.projectnaptha.com/img/chi_sim.png",
    "https://www.w3.org/Icons/w3c_home.png",
    "https://example.com/"
  ],
  "language": "en+zh-cn",
  "translateTo": "en",
  "maxItems": 100
}
```

Or start a run over the Apify API:

```bash
curl -X POST "https://api.apify.com/v2/acts/Pradio~image-text/runs?token=<YOUR_APIFY_TOKEN>" \
  -H "Content-Type: application/json" \
  -d '{"image_urls": ["https://tesseract.projectnaptha.com/img/eng_bw.png"], "language": "eng"}'
```

### Input

| Input | Type | Default | What it does |
|---|---|---|---|
| `image_urls` | array of strings | prefilled | The public http(s) image URLs the run reads. One data row per readable entry. |
| `base64Images` | array of objects | empty | Images sent inside the input, each an object with a `name` and the file in base64 as `data`. |
| `keyValueStoreId` | string | empty | One of your key-value stores that holds images, picked by ID or name. |
| `keyValueStoreRecords` | array of strings | empty | The keys of the files to read from that store, one per line. |
| `outputDetail` | string | `full` | `full`, `lines` or `text`: how much layout each row carries. |
| `translateTo` | string | `en` prefilled, off when empty | `en` adds an English machine translation of the read text. Empty skips translation. |
| `language` | string | `en+zh-cn` prefilled, `eng` when empty | The language code the OCR reads in. It also picks the engine. With `en`, text in other languages written in the Latin alphabet is still read, but accented letters come back plain (Paraná reads Parana). Text in another script, such as Arabic or Cyrillic, comes back free as an `unreadable` miss, with the text read kept on the row: name its language and run again. |
| `maxItems` | integer | `100` | The most rows one run returns. Entries past the cap are not pushed or charged. |
| `maxMegabytesPerImage` | integer | `40` | The most megabytes one image may be (1 to 40). Each entry is capped on its own. |

#### image\_urls

The list of public http(s) image URLs to read. Each entry is fetched with a plain GET and passed to the OCR engine. Entries can be plain URL strings or objects with a `url` field. An entry that is not a URL, not http(s), not image bytes, too large, refused or too slow becomes a named, uncharged status row instead of a data row.

#### base64Images

Images that are not online, sent inside the input itself. Each entry is an object with a `name` and the image's base64 `data`, bare or as a `data:image/png;base64,` URI. The name comes back on the row as `image_name`. An entry that is not an object, not valid base64 or not image bytes becomes an uncharged `bad_input` row naming it. The platform limits the size of a run's input, so this suits small images such as receipts and screenshots; send large files through a key-value store.

```json
{
  "base64Images": [
    { "name": "receipt-0142.png", "data": "<the file in base64>" }
  ],
  "language": "en"
}
```

#### keyValueStoreId and keyValueStoreRecords

Images already saved in one of your Apify key-value stores. Pick the store in **Key-value store** (this is what gives the run read access to it) and list the keys to read. Each key may hold the image file itself (PNG, JPEG, GIF, BMP, WebP or TIFF) or its base64 text. Every key comes back as a row: a data row, `not_found` when the store holds nothing under that key, `blocked` when the run was not given access to the store, or `bad_input` when what is stored is not an image. `image_name` is `store/key`.

```json
{
  "keyValueStoreId": "my-scans",
  "keyValueStoreRecords": ["invoice-2026-001.jpg", "invoice-2026-002.jpg"],
  "language": "en"
}
```

#### outputDetail

How much layout each row carries. `full`, the default, returns every word, line and paragraph with its box. `lines` sets `words` to null, the largest part of a row, and keeps lines and paragraphs. `text` keeps `full_text`, `word_count` and `mean_confidence` and sets `words`, `lines` and `paragraphs` to null. The price per image is the same at every level.

```json
{
  "image_urls": ["https://raw.githubusercontent.com/naptha/tesseract.js/master/tests/assets/images/testocr.png"],
  "outputDetail": "text"
}
```

#### translateTo

Leave it empty to skip translation, or set it to `en` for an English translation of each image's text. See Translation below for how the language is chosen and what each row says.

#### language

The language code the OCR reads in: `eng`, `deu`, `fra`, `spa` and so on. `eng`, `chi_sim` and `chi_tra` (alone or joined) read with PaddleOCR; every other code reads with Tesseract. Common two-letter codes are mapped for you (`en`, `de`, `fr`, `es`, `it`, `pt`, `ar`, `nl`, `pl`, `ja`, `ko`, `zh-cn`, `zh-tw`, `uk`, `ru`), and codes joined with `+` such as `eng+fra` read mixed-language images. A malformed value returns one `bad_input` row naming it before any image is fetched, and a code the engine cannot load fails the run's entries with a named miss.

#### maxItems

The most rows one run returns, default 100. Entries past the cap are dropped before anything is pushed or charged. The run's log notes the cap, and RUN\_SUMMARY's fetched and pushed counts show what was left out.

#### maxMegabytesPerImage

The most megabytes a single image may declare or download before the run reads it: 1 to 40, default 40. Each entry is capped on its own, so one oversized image cannot starve the rest of the list. An image over the cap becomes an uncharged `bad_input` row naming it, and the run moves to the next. Lower it to bound what one entry may cost; it cannot be raised past 40.

### Translation

Translation is machine translation, into English only, from eight languages: German, French, Spanish, Italian, Dutch, Polish, Russian and Chinese. It costs nothing extra. The Console form comes with `en` already set, so a first run from the form translates; clear the field to skip it. An API call that leaves `translateTo` out does not translate.

- **The language comes from the text.** Each row's `detected_language` picks the translation model. The language is never guessed: when it could not be detected (under 20 letters, or too mixed), `translation` is null and `translation_note` says so.
- **English is left alone.** Text detected as English gets `translation_note`: `already English`.
- **Other languages are named.** Text in a language without a model, Portuguese for example, gets `translation_note`: `no translation model for language por`.
- **Read in the right language first.** Translation works from `full_text`, so a misread word is translated as misread. Setting `language` to the image's own language (`de` for German) reads its accents and letters, and the translation is better for it. Filter on `mean_confidence` as you would for the text.
- **Invented passages are held back.** A translation far longer than the text read is a sign the model invented it. Such a passage is kept as read, untranslated, and `translation_note` counts it.
- **Up to 5,000 characters per image** of the read text are translated; `translation_note` says when a longer text was cut.
- **On this machine, not a service.** The models are built into the Actor, so a run sends your text nowhere, and the run needs no API key.

Use it to read German text and get it in English beside the German. This scanned carol was read with `language` set to `de`. Its row came back with `detected_language` `deu` and a `translation` that opens "O fir tree, How faithful are your leaves".

```json
{
  "image_urls": ["https://tesseract.projectnaptha.com/img/deu.png"],
  "language": "de",
  "translateTo": "en",
  "outputDetail": "text"
}
```

The translation models are Opus-MT by the Language Technology Research Group at the University of Helsinki (Helsinki-NLP), used in their ONNX conversions by Xenova. The German, French, Spanish, Italian, Dutch and Polish models are licensed under Apache 2.0, and the Russian and Chinese models under CC BY 4.0.

### Worked examples

Use it to read a batch of public images, text only. Smaller rows suit a search index that needs the words and not their positions.

```json
{
  "image_urls": [
    "https://raw.githubusercontent.com/naptha/tesseract.js/master/tests/assets/images/testocr.png",
    "https://raw.githubusercontent.com/naptha/tesseract.js/master/tests/assets/images/cosmic.png"
  ],
  "language": "en",
  "outputDetail": "text"
}
```

Use it to read German text and check the language. Compare `detected_language` (`deu`) with `language` to spot a page in another language.

```json
{
  "image_urls": ["https://tesseract.projectnaptha.com/img/deu.png"],
  "language": "de",
  "outputDetail": "lines"
}
```

Use it to read a receipt and keep every word's position. The full detail carries a box and a confidence score per word, cut from its line on English text.

```json
{
  "image_urls": ["https://raw.githubusercontent.com/naptha/tesseract.js/master/tests/assets/images/bill.png"],
  "language": "en",
  "outputDetail": "full"
}
```

Use it to send screenshots from your own app without hosting them. Each image goes in as base64 with a name that comes back on its row.

```json
{
  "base64Images": [
    { "name": "ticket-8841.png", "data": "<the file in base64>" },
    { "name": "ticket-8842.png", "data": "<the file in base64>" }
  ],
  "language": "en"
}
```

Use it to read files an earlier run saved to your key-value store. Every key comes back as a named row.

```json
{
  "keyValueStoreId": "my-scans",
  "keyValueStoreRecords": ["page-001.png", "page-002.png", "page-003.png"],
  "language": "en",
  "maxItems": 100
}
```

### Output

A run ends in one of these shapes, and each is named in the dataset:

- **Data rows** (`row_type`: `ROW`, `status`: `ok`): one per readable image, charged.
- **Miss rows** (`row_type`: `ITEM_STATUS`): one per entry that could not be read, carrying `status`, `image_url` and `reason`. Pushed so you see the verdict, never billed.
- **One NO\_TEXT\_FOUND row**: the run had nothing to return. This is the zero answer, an explanation instead of an empty dataset. Not billed.
- **One STOPPED\_EARLY row**: the per-run charge limit was reached. `rowsReturned` and `rowsRemaining` say how far the run got. Not billed.

A zero result always means something specific. An empty input list produces the NO\_TEXT\_FOUND row with `rowsFetched` at zero. A readable image with no text in it is different: it is a data row with `status` `ok` and an empty `full_text`, billed like any other read because the OCR ran and "no text" is its honest answer.

For the counts behind any run, open the RUN\_SUMMARY entry in the run's default key-value store: rowsFetched, rowsPushed, rowsCharged, rowsUncharged, duplicatesDropped and stoppedEarly, beside the input that produced them.

### What can you do with the data?

**Digitise a pile of scans.** An operations specialist points a run at photographed receipts, statements and forms, and gets the text of each back as a row ready for a spreadsheet or an accounting pipeline.

**Index image content for search.** A developer feeds a folder of screenshots or scanned pages through the API and indexes `full_text`, keeping the bounding boxes so hits can be highlighted on the image itself.

**Catalogue a collection.** A researcher pulls labels, captions and identifiers out of archived images. The word, line and paragraph structure keeps enough layout to tell a heading from a footnote.

**Give an agent eyes.** A builder wiring an AI agent registers this Actor over MCP, and the agent can read the text inside any public image URL it is handed.

### Use this OCR API with AI agents

Paste this line to give a compatible agent this Actor as a tool:

```bash
claude mcp add --transport http apify "https://mcp.apify.com?tools=Pradio/image-text"
```

### Personal data

The text in your images may contain personal data, and the design keeps that your business and nobody else's. For transparency, this note says plainly what may be in the rows, who controls them and what happens to them:

- **Only the images you give it.** It reads the URLs you list, the base64 data you send and the key-value store keys you name. It follows no links and fetches nothing else.
- **No retention beyond the run.** Extracted text is returned to your dataset. The images and their text are not kept or reused after the run; the run keeps only a `RUN_SUMMARY` of counts in its key-value store.
- **No inference on the text.** Extraction is verbatim. No enrichment, profiling, categorisation or model training runs on row content. `detected_language` names the language of the text as a whole, and nothing about any person in it.
- **Translation stays inside the run.** When you turn on `translateTo`, the text is translated by models built into the Actor. It is not sent to any outside service, and nothing is kept after the run.
- **You are the controller.** You decide what to upload, so send only images you have the right to process, and turn on translation only for text you have the right to translate. A translation is a copy of the text in another language.
- **A refused read stops.** A 401, 403 or 429, or any failed fetch, ends that read. Nothing is retried past a block, and no login, captcha or access wall is defeated.

### Release notes

- **2026-09-25**: a read that forms no words in a detectable language at a confidence under 70 now comes back as a free `unreadable` miss instead of a billed row. The text read stays on the row.
- **2026-09-25**: new `translateTo` input. Set it to `en` for an English machine translation of text in German, French, Spanish, Italian, Dutch, Polish, Russian or Chinese, in the new `translation`, `translation_language` and `translation_note` columns. No extra charge; an empty field skips it.
- **2026-09-24**: images can now be sent as base64 (`base64Images`) or read from your key-value store (`keyValueStoreId`, `keyValueStoreRecords`). New `image_source`, `image_name` and `detected_language` columns, and an `outputDetail` input for smaller rows.
- **2026-09-24**: English and Chinese now read with PaddleOCR. On the same 57 unseen images, 51 reads now score 60 or more in confidence where 15 did before. New `ocr_engine` column, and the default memory is now 2048 MB.
- **2026-09-24**: new `mean_confidence` column on every row, so low-confidence reads from photographs can be filtered out.
- **0.1** (2026-09-22): first build.

### Limits

- **Public image URLs, or the bytes themselves.** URL entries must be http(s) links that return image bytes. An HTML page or a non-image response is a `bad_url` miss, and anything behind a login is outside this Actor by design; send such a file as base64 or through your key-value store instead.
- **Key-value store access.** The run reads only the store picked in the Key-value store field. A store named any other way may be refused, and each of its keys comes back `blocked` with the reason.
- **Translation is machine translation.** It is into English only, from the eight languages listed under Translation. It is only as good as the text read, so noisy text from a stylised poster translates as noise.
- **A wrong-script read is a free miss.** Arabic read with `en` used to come back as noise on a billed row. Now a read with no detected language and a `mean_confidence` under 70 is an uncharged `unreadable` miss. It keeps `full_text`, so you can see what was read. A short, clear read such as a logo or a number that scores 70 or more stays a billed `ok` row.
- **Detected language on longer text.** `detected_language` needs 20 letters or more and reads the text as a whole, so a short label or a page mixing languages comes back null or as its main language. Close relatives can be confused when the text was read without its accents: a Portuguese certificate read with `en` came back as Galician (`glg`). Name the image's own language (`pt`) and the accents, and the detection, come back right.
- **Direct fetch only.** No browser and no proxy. A host that answers only a real browser, or only some regions, is a named miss rather than a worked-around read.
- **Refused reads are never retried.** HTTP 401, 403 and 429 come back as `blocked`, and a 404 as `not_found`. Other HTTP errors and dead hosts come back as `fetch_failed`, and a host that does not answer in time as `timeout`.
- **Per-image bounds.** An image over the byte cap is a `bad_input` miss, and a fetch that runs too long is a `timeout`.
- **Dimensions on five formats.** PNG, JPEG, GIF, BMP and WebP carry `image_width` and `image_height`. On the PaddleOCR path TIFF carries them too, read by the image decoder. On the Tesseract path TIFF dimensions stay null, and an unusual format still goes to the OCR engine when the host calls it an image.
- **Chinese comes back line by line in `words`.** English and Chinese are read by PaddleOCR, which reads whole lines, and a line is split into words on its spaces. Chinese is written without spaces, so each unit in `words` is a line (or a run of characters between spaces), and `word_count` counts those units, not words.
- **Stray Chinese characters in Latin text.** PaddleOCR reads English with its one recognition model, which knows Chinese and Latin letters together. A smudge or an ornament on a poster or a screenshot can come back as a Chinese character or a full-width bracket inside Latin text. They are not removed, because a bilingual sign read with `en` carries real Chinese on the same row. For text in another Latin-script language, name that language (`it`, `fr`, `pt`): Tesseract reads it, and its models carry no Chinese characters.
- **OCR is exactness-limited.** Handwriting, decorative fonts and low-contrast text can come back partial. The confidence score on every word, line and paragraph is the signal to read.
- **A readable image with no text still bills.** The OCR ran and "no text" is the delivered answer, so the row is `ok` and charged like any other data row.
- **Errors run toward named misses and loud failures.** A problem on one entry becomes a status row; a problem with the run itself fails the run. Nothing comes back as silent nulls, which is the safer direction for a pipeline.

### Troubleshooting

**I sent six URLs but the dataset shows five data rows and one odd row.**

Working as intended. The odd row is an ITEM\_STATUS row: that entry missed, and its `status` and `reason` say why. In the sample above the sixth URL returned a web page, not image bytes, so it came back `bad_url` and uncharged. The five data rows include one image with no text in it (a logo), which comes back with `full_text` as an empty string and `word_count` 0.

**The dataset has one row and row\_type is NO\_TEXT\_FOUND.**

The run had nothing to return: the input list was empty or produced no usable entries. That is the zero answer, not a crash. `rowsFetched` on the row says how many entries were read.

**A row's full\_text is empty but its status is ok.**

The image was fetched and read, and the OCR found no text. That is the honest answer for a photograph or a blank scan, and `word_count` will be 0. It is billed because the OCR ran.

**I pasted a list with repeats and got fewer rows than entries.**

Duplicate URLs are dropped before pushing and are never billed. Paste 50 entries with three repeats and you get 47 rows. RUN\_SUMMARY's `duplicatesDropped` carries the count for your run.

**The last row says STOPPED\_EARLY.**

The run hit the per-run charge limit set on it. `rowsReturned` and `rowsRemaining` say how far it got. Raise the limit and run again for the rest.

**My run failed instead of returning rows.**

A failure the run cannot attribute to one entry fails the whole run with the error in the log. That is deliberate: partial nulls would look like a good run that found little.

If none of these match what you see, open an issue on this Actor's Store page with the run ID and the RUN\_SUMMARY entry.

### FAQ

**Can I use integrations with OCR Image to Text API?**

Yes. It is a standard Apify Actor: every run writes its rows to an Apify dataset, which the platform's integrations, exports and other Actors can read. Point the integration at the run's dataset and the rows flow through like any other Actor's output.

**Can I use OCR Image to Text API with the Apify API?**

Yes. The API can start runs, poll their status and read dataset items, so the Actor drops into an existing pipeline without the Console. The curl line under "How do I use" is the whole call. Schedules and webhooks apply to it like any Apify Actor.

**Can I use OCR Image to Text API through an MCP server?**

Yes. The snippet in the AI-agents section above registers it as a tool with an MCP-aware agent, after which the agent can hand it image URLs and read the text rows back.

**Is it legal to read the text in these images?**

You supply the URLs, and the Actor reads only the images at those URLs, fetched the way any visitor without a login would fetch them. You are the controller of what you send: process only images you have the right to process. A host that refuses the read is reported and never worked around, and no content is kept past the run.

### See also

More Actors from the same publisher:

- [Website Email Phone Contacts Scraper](https://apify.com/Pradio/website-email-phone): the email addresses and phone numbers a company's own website publishes, one row per site.
- [Companies House](https://apify.com/Pradio/companies-house): UK company register profiles and filings, one row per company.

### Report a problem

A URL that parses wrong, an image the reader should have handled, a field that should be there? Open an issue on this Actor's Issues tab in the Apify Console. That is the fastest way to get it fixed, and the report is never a bother.

### Not affiliated

OCR Image to Text API is an independent tool. It is not affiliated with, endorsed by or sponsored by any host whose images you point it at, or by the maintainers of the OCR engine it runs. You choose the URLs; it reads them on your behalf.

# Actor input Schema

## `image_urls` (type: `array`):

The public http(s) image URLs this run reads. Each entry is fetched and read with OCR into one data row; an entry that cannot be read becomes an uncharged status row naming the miss. Leave it empty when you send images through base64Images or a key-value store instead.

## `language` (type: `string`):

The language code the OCR reads in (eng when left empty; English and Chinese are read with PaddleOCR, other languages with Tesseract). Two-letter codes like en or de are mapped for you, and codes joined with + read mixed-language images. A code the engine cannot load fails the run's entries and says so.

## `base64Images` (type: `array`):

Images sent inside the input itself, for files that are not online. Each entry is an object with a name and the image's base64 data, bare or as a data: URI, for example {"name": "receipt-01.png", "data": "<the file in base64>"}. The name is echoed on the row as image\_name. An entry that is not valid base64 image data becomes an uncharged bad\_input row naming it. The whole input is size-limited by the platform, so send large files through a key-value store.

## `keyValueStoreId` (type: `string`):

One of your key-value stores holding the images to read, by its ID or name. Pick it here so the run is given read access to it. Records are named in keyValueStoreRecords.

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

The record keys to read from the key-value store above, one per line. Each record should hold an image file (PNG, JPEG, GIF, BMP, WebP or TIFF) or its base64 text. A key the store does not hold becomes an uncharged not\_found row naming it.

## `outputDetail` (type: `string`):

How much layout each row carries. full (the default) returns words, lines and paragraphs with their boxes; lines drops the per-word list, the largest part of a row; text returns the full text and the counts only, with words, lines and paragraphs set to null.

## `translateTo` (type: `string`):

Leave empty to skip translation. Set to en to add an English machine translation of each image's text in the translation field. The text's language is detected from what was read, and German, French, Spanish, Italian, Dutch, Polish, Russian and Chinese are translated. Text already in English, or in another language, is left untranslated and translation\_note says why. Translation runs inside the Actor: no outside service sees your text, and there is no extra charge.

## `maxItems` (type: `integer`):

The most rows one run returns (default 100). Entries past the cap are dropped before pushing or charging, and the run's summary shows what was left out.

## `maxMegabytesPerImage` (type: `integer`):

The most megabytes one image may declare before the run reads it (1 to 40, default 40). Each entry is capped on its own: an image over the cap becomes an uncharged bad\_input row naming it, and the run moves to the next.

## Actor input object example

```json
{
  "image_urls": [
    "https://tesseract.projectnaptha.com/img/eng_bw.png",
    "https://raw.githubusercontent.com/naptha/tesseract.js/master/tests/assets/images/bill.png",
    "https://raw.githubusercontent.com/naptha/tesseract.js/master/tests/assets/images/cosmic.png",
    "https://tesseract.projectnaptha.com/img/chi_sim.png",
    "https://www.w3.org/Icons/w3c_home.png",
    "https://example.com/"
  ],
  "language": "en+zh-cn",
  "base64Images": [],
  "keyValueStoreRecords": [],
  "outputDetail": "full",
  "translateTo": "en",
  "maxItems": 100,
  "maxMegabytesPerImage": 40
}
```

# Actor output Schema

## `rows` (type: `string`):

The text rows for this run: one per readable image, plus any uncharged status rows.

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

This run's counts: entries fetched, rows pushed, events charged, duplicates dropped, and whether the charge limit stopped the run early.

# 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 = {
    "image_urls": [
        "https://tesseract.projectnaptha.com/img/eng_bw.png",
        "https://raw.githubusercontent.com/naptha/tesseract.js/master/tests/assets/images/bill.png",
        "https://raw.githubusercontent.com/naptha/tesseract.js/master/tests/assets/images/cosmic.png",
        "https://tesseract.projectnaptha.com/img/chi_sim.png",
        "https://www.w3.org/Icons/w3c_home.png",
        "https://example.com/"
    ],
    "language": "en+zh-cn",
    "translateTo": "en"
};

// Run the Actor and wait for it to finish
const run = await client.actor("pradio/image-text").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 = {
    "image_urls": [
        "https://tesseract.projectnaptha.com/img/eng_bw.png",
        "https://raw.githubusercontent.com/naptha/tesseract.js/master/tests/assets/images/bill.png",
        "https://raw.githubusercontent.com/naptha/tesseract.js/master/tests/assets/images/cosmic.png",
        "https://tesseract.projectnaptha.com/img/chi_sim.png",
        "https://www.w3.org/Icons/w3c_home.png",
        "https://example.com/",
    ],
    "language": "en+zh-cn",
    "translateTo": "en",
}

# Run the Actor and wait for it to finish
run = client.actor("pradio/image-text").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 '{
  "image_urls": [
    "https://tesseract.projectnaptha.com/img/eng_bw.png",
    "https://raw.githubusercontent.com/naptha/tesseract.js/master/tests/assets/images/bill.png",
    "https://raw.githubusercontent.com/naptha/tesseract.js/master/tests/assets/images/cosmic.png",
    "https://tesseract.projectnaptha.com/img/chi_sim.png",
    "https://www.w3.org/Icons/w3c_home.png",
    "https://example.com/"
  ],
  "language": "en+zh-cn",
  "translateTo": "en"
}' |
apify call pradio/image-text --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,pradio/image-text"
        }
    }
}
```

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/89dBdHIn6CiI8sH0C/builds/bHCdY6c8QT6UfuheP/openapi.json
