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

OCR Image to Text API: Words, Boxes, Confidence

Pricing

from $2.80 / 1,000 image returneds

Go to Apify Store
OCR Image to Text API: Words, Boxes, Confidence

OCR Image to Text API: Words, Boxes, Confidence

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.

Pricing

from $2.80 / 1,000 image returneds

Rating

0.0

(0)

Developer

Pradio Actors

Pradio Actors

Maintained by Community

Actor stats

0

Bookmarked

2

Total users

1

Monthly active users

6 hours ago

Last modified

Share

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

BuyerWhat they run it for
Developers adding OCR to an appPost a list of image URLs through the API and get JSON rows back, one per image
Operations and back-office teamsTurn scanned documents, receipts and screenshots into text they can search and process
Data and research teamsPull the text out of image collections for indexing, cataloguing or analysis
AI-agent buildersGive 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.

{
"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:

FieldTypeWhat it is
image_urlstringThe 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_sourcestringWhere the image came from: url, base64 or key_value_store.
image_namestringThe 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_textstringThe whole text the OCR read out of the image. An empty string means the image was readable but carried no text.
paragraphsarrayThe paragraphs the OCR found. Each entry carries text, a confidence score and a bbox pixel box. Null when outputDetail is text.
word_countnumberHow many words the OCR read, the length of words. Zero on a readable image with no text.
mean_confidencenumberThe 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_widthnumberThe image's width in pixels, sniffed from the file's own header. Null when the format's dimensions are not sniffed.
image_heightnumberThe image's height in pixels, sniffed the same way as image_width.
wordsarrayEvery 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.
languagestringThe 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_languagestringThe 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_enginestringWhich engine read this image: paddleocr for English and Chinese, tesseract for every other language you name.
linesarrayThe text lines the OCR found. Each entry carries text, a confidence score and a bbox pixel box. Null when outputDetail is text.
translationstringThe 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_languagestringen when a translation was made, null otherwise.
translation_notestringWhy 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_typestringROW 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.
statusstringThe 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.
reasonstringStatus rows only: the miss reason on ITEM_STATUS, why nothing was returned on NO_TEXT_FOUND, or why the run stopped early.
rowsFetchedintegerStatus rows only: how many entries the run read before de-duplication and the cap.
rowsReturnedintegerStatus rows only: how many data rows are in the dataset.
rowsRemainingintegerStatus 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:

{
"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 sendBilled rows backImage costStart event
100 image URLsabout 93$0.37$0.0001
1,000 image URLsabout 930$3.72$0.0001
10,000 image URLsabout 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:

{
"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:

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

InputTypeDefaultWhat it does
image_urlsarray of stringsprefilledThe public http(s) image URLs the run reads. One data row per readable entry.
base64Imagesarray of objectsemptyImages sent inside the input, each an object with a name and the file in base64 as data.
keyValueStoreIdstringemptyOne of your key-value stores that holds images, picked by ID or name.
keyValueStoreRecordsarray of stringsemptyThe keys of the files to read from that store, one per line.
outputDetailstringfullfull, lines or text: how much layout each row carries.
translateTostringen prefilled, off when emptyen adds an English machine translation of the read text. Empty skips translation.
languagestringen+zh-cn prefilled, eng when emptyThe 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.
maxItemsinteger100The most rows one run returns. Entries past the cap are not pushed or charged.
maxMegabytesPerImageinteger40The 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.

{
"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.

{
"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.

{
"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".

{
"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.

{
"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.

{
"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.

{
"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.

{
"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.

{
"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:

$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:

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.