# Bulk OCR: Google Sheet or CSV of Image & PDF URLs to Text (`brasspage/bulk-ocr-google-sheets-csv`) Actor

Bulk OCR for a Google Sheet or CSV with a column of image or PDF URLs. Batch image to text: each row comes back with your columns plus the text, page counts and OCR confidence. Tesseract 5 runs inside the Actor, no outside AI. $0.005 per OCR page, $0.0005 per digital page. Failed files are free.

- **URL**: https://apify.com/brasspage/bulk-ocr-google-sheets-csv.md
- **Developed by:** [Brasspage](https://apify.com/brasspage) (community)
- **Categories:** AI, Developer tools, Automation
- **Stats:** 2 total users, 1 monthly users, 0.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $5.00 / 1,000 ocr pages

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

### Bulk OCR: Google Sheet or CSV of Image & PDF URLs to Text

Have a column of image or PDF URLs? Paste the Google Sheet or CSV link here. The Actor reads every file and gives your table back with the text in a new column. **$0.005 per OCR page, $0.0005 per digital page. Rows with no link, failed files and blank pages are free.**

This is a real run on a public Google Sheet (Oct 31, 2026), shortened:

| id  | book                     | image_url                          | ocrText (the new column)                                                    | ocrConfidence |
| --- | ------------------------ | ---------------------------------- | --------------------------------------------------------------------------- | ------------- |
| 1   | On the Origin of Species | `https://.../Origin_of_....jpg`    | THE ORIGIN OF SPECIES BY MEANS OF NATURAL SELECTION... By CHARLES DARWIN... | 88.4          |
| 4   | Frankenstein             | `https://.../Frankenstein_....jpg` | FRANKENSTEIN ; OR, THE MODERN PROMETHEUS. IN THREE VOLUMES...               | 82.8          |
| 5   | A row with no link yet   |                                    | empty, with "No http(s) link in this row." in `ocrError`, and no charge     |               |

- **Your columns stay.** Each output row holds every input column, then `ocrText`, the page counts, the OCR confidence and any warnings. The rows come back in the order of your table, so a CSV or Excel export lines up with your sheet row by row.
- **Paste a Google Sheets link.** Share the sheet as "Anyone with the link" and paste the link from your browser. No Google sign-in and no API key.
- **Google Drive and Dropbox links work.** Share links in the cells are turned into direct downloads.
- **Scans and photos work.** Tesseract 5, the open source OCR engine, runs inside the Actor. Pages scanned sideways or upside down are turned upright first. Digital PDF pages are read from their own text layer.
- **Nothing leaves the run.** No page is sent to an outside AI service or any other API.

Use it for batch OCR of scanned invoices, receipts, forms, product photos and archive scans: any list of files where you need image to text or PDF to text next to your own data.

**Try it now.** The input comes prefilled with a sample CSV of 3 links: the 6-page IRS Form W-9 (a digital PDF), a scanned invoice page and a PNG screenshot of a table. Click **Start**. The run costs about $0.0132: 6 text layer pages ($0.003), 2 OCR pages ($0.010) and the start ($0.0002 at the default 4 GB).

#### How to OCR a Google Sheet

1. Put one image or PDF URL in each row of a column. The first row holds the column names.
2. In Google Sheets, click **Share** and set General access to "Anyone with the link".
3. Paste the sheet link into **Google Sheet or CSV link** and click **Start**.
4. Download the result as CSV or Excel, or read it from the API.

### Pricing

Pay per event, no monthly fee, no fee per file.

| Event                                                       | Price                                                            |
| ----------------------------------------------------------- | ---------------------------------------------------------------- |
| Actor start                                                 | $0.00005 per run, per GB of memory ($0.0002 at the default 4 GB) |
| `ocr-page`: a scanned page or an image that gives text      | $0.005                                                           |
| `text-layer-page`: a digital PDF page that gives text       | $0.0005                                                          |
| A row with no link, a failed file, a blank or a failed page | free                                                             |

Worked examples, start fee not included:

| Table                                                             | Text layer pages | OCR pages | Total  |
| ----------------------------------------------------------------- | ---------------- | --------- | ------ |
| The prefilled sample (3 rows)                                     | 6                | 2         | $0.013 |
| 200 phone photos of receipts, 1 image each                        | 0                | 200       | $1.00  |
| 500 digital invoices, 2 pages each                                | 1,000            | 0         | $0.50  |
| 1,000 scanned 4-page forms, only page 1 read (`pages` set to `1`) | 0                | 1,000     | $5.00  |

**Your maximum cost is a hard stop.** Set **Maximum cost per run** in the run options. A page starts only if the pages already read, every page in progress and this page, each at the OCR price, still fit. When the next page does not fit, the run stops under your limit, and every row after it gets "Skipped: the run's maximum cost was reached." in `ocrError`. Run again on those rows later.

### Input

| Field            | What it does                                                                                                                               |
| ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
| `sheetUrl`       | A Google Sheets link or a link to a CSV file. The first row must hold the column names.                                                    |
| `csvFile`        | A CSV file from your computer, used when `sheetUrl` is empty. Comma, semicolon and tab separated files all work.                           |
| `datasetId`      | One of your Apify datasets, for example a scraper's output. Each item is a row. When set, it is used first.                                |
| `urlColumn`      | The column with the image or PDF URLs. Empty: the column with the most `http(s)` links.                                                    |
| `pages`          | Pages to read in each file, for example `1` or `1-3, 5`. Empty: all pages.                                                                 |
| `language`       | The language of the scanned pages: English, Spanish, French, German, Portuguese, Russian, Arabic, Hindi, Chinese (simplified) or Japanese. |
| `outputFormat`   | `text` (default) or `markdown` (headings, lists and tables).                                                                               |
| `maxPagesPerRun` | Safety limit on pages per run, default 5,000.                                                                                              |

A Google Sheets link can be the normal edit link, a view link or a "Publish to web" link. For a sheet with several tabs, open the tab first and copy the link then: the link carries the tab.

```json
{
  "sheetUrl": "https://docs.google.com/spreadsheets/d/<your sheet id>/edit#gid=0",
  "urlColumn": "image_url",
  "pages": "1"
}
```

### Output

One dataset item per input row, in the order of your table:

| Field               | What it holds                                                                                                     |
| ------------------- | ----------------------------------------------------------------------------------------------------------------- |
| your columns        | Every input value as it was. A column that has the name of an OCR field is kept as `<name> (input)`.              |
| `ocrText`           | The text of the file, pages in order. Up to 30,000 characters, so it fits a spreadsheet cell.                     |
| `ocrPageCount`      | Pages in the file. An image is 1 page.                                                                            |
| `ocrPages`          | Pages read with OCR that gave text (charged as `ocr-page`).                                                       |
| `ocrTextLayerPages` | Digital pages read from the text layer that gave text (charged as `text-layer-page`).                             |
| `ocrConfidence`     | The mean Tesseract word confidence of the OCR pages, 0 to 100. Under about 70 usually means a poor scan or photo. |
| `ocrWarnings`       | One line per warning: a table number that may be misread, or text cut to 30,000 characters.                       |
| `ocrFileUrl`        | Only for text over 30,000 characters: a link to the full text file.                                               |
| `ocrError`          | Why the row gave no text, for example a private link or a web page instead of a file. Empty when the row worked.  |

The **OCR results** tab shows the OCR fields only. **All fields** and every export (CSV, Excel, JSON) hold your columns too. JSON keeps your column order. In CSV and Excel exports Apify sorts the columns by name; add `&fields=id,name,...` to the export URL to set the order. A cell in Excel holds at most 32,767 characters and one in Google Sheets 50,000, so `ocrText` stops at 30,000.

The text is not written back into your sheet. Download the CSV or Excel export and paste the new columns next to yours, or import the file as a new tab.

### Limits

- **Handwriting is weak.** Tesseract reads printed and typed text. Handwritten notes mostly come out wrong or empty.
- **Phone photos read worse than scans.** Shadows, a tilt and low resolution all cost accuracy. Check `ocrConfidence`: a photo that scores under about 70 needs a closer look.
- **Links behind labels are lost.** If a cell shows a label such as "Invoice" with the link behind it (an inserted link or a `HYPERLINK` formula), the Google Sheets CSV export holds only the label. Put the plain link in the cell.
- **The sheet must be public.** Private sheets need a Google sign-in, which the Actor does not do. Set General access to "Anyone with the link", or use File > Share > Publish to web as CSV.
- **Google Drive files must be shared** as "Anyone with the link". Very large Drive files open a virus scan page instead of the file and fail.
- **Excel files are not read yet.** Save the sheet as CSV first.
- **Up to 10,000 rows per run**, 100 MB per file, 5,000 pages per PDF. A link must point to the file itself, not to a web page that shows it.
- **Busy hosts.** If a file host answers HTTP 429 or 503, the row gets two more tries, after 5 and 20 seconds. A run waits at most 120 seconds in total. Some hosts keep refusing cloud servers: in our test, one Wikimedia image answered 429 on every try from Apify. Such a row gets the error in `ocrError`, free.
- **Public hosts only.** Links to private networks, local addresses and cloud metadata addresses are refused.
- **Files:** PDF, PNG, JPEG, WEBP and multi-page TIFF. Password-protected PDFs fail (free).
- **Speed:** files are read one after another. On Apify, our scanned invoice page (a 13-row table) took about 10 seconds at the default 4 GB (one full CPU core) and about 40 seconds at 1 GB, 8 pages in each run (Oct 31, 2026). Pages with a text layer are much faster. The default timeout of 4 hours fits about 1,400 pages like ours. For more, raise the timeout. If a run times out, resurrect it: it goes on from the next row.

### Use it from the API

```bash
curl -X POST "https://api.apify.com/v2/acts/brasspage~bulk-ocr-google-sheets-csv/runs?token=<YOUR_TOKEN>" \
  -H "Content-Type: application/json" \
  -d '{"sheetUrl": "https://docs.google.com/spreadsheets/d/<your sheet id>/edit", "pages": "1"}'
```

When the run finishes, read the rows from its default dataset (`/v2/datasets/<id>/items?format=csv` gives CSV). To give an AI agent this tool, connect it to the Apify MCP server at `https://mcp.apify.com?tools=brasspage/bulk-ocr-google-sheets-csv`.

### How accurate is it?

This Actor runs the same engine as PDF to Markdown OCR and PDF Text Extractor. Our own test files, Oct 29 to 31, 2026:

| Test                                                | Result                                                                |
| --------------------------------------------------- | --------------------------------------------------------------------- |
| Typed 1942 government page, scanned at 300 dpi      | 98.5% of words match a reference transcript                           |
| 24 made-up scanned invoices, 150 to 300 dpi         | 810 of 810 numbers exact                                              |
| 102 test pages, each turned 90, 180 and 270 degrees | all 306 give the same text as the upright page                        |
| The prefilled sample: scanned invoice and PNG table | mean confidence 94.5 and 95.6                                         |
| 4 title pages from 1818 to 1859, ornate old type    | mean confidence 62.3 to 88.4, plain lines right, ornate lines garbled |

Most of these are clean, printed test pages. Your own scans and photos can read worse.

### FAQ

**Where do my files go?**
Nowhere outside the run. The Actor downloads each file, reads it inside the container and writes the rows to your own Apify storage. No page goes to an AI model or any other outside service.

**Can text in a file or a cell change what the Actor does?**
No. There is no AI model to give instructions to. Text in a cell is only read as a link, and text in a file is only output as text.

**A run stopped part way. Do I pay twice if I run it again?**
Each run charges only the pages it reads. If Apify moves a run to another server, or you resurrect a run that timed out, it goes on from the next row, so no row is charged twice in that run.

**Who is responsible for the rights to the files?**
You are. Only process files that you own, that are public, or that you have permission to use. The Actor does not get around logins, paywalls or PDF passwords.

### Issues and support

Something does not work, or you need a feature (Excel input, or the text written back into the sheet)? Open an issue in the Actor's **Issues** tab, with the run URL if you have one. We reply there.

### Related Actors

- [PDF Text Extractor](https://apify.com/brasspage/pdf-text-extractor): the same engine for a list of PDF links, one item per PDF.
- [PDF to Markdown OCR](https://apify.com/brasspage/pdf-ocr-to-markdown): the same engine, with Markdown headings, lists and tables for LLMs and RAG.
- [Google Sheets Export](https://apify.com/brasspage/google-sheets-sync): append or upsert dataset rows into a Google Sheet.

Built and maintained by Tack, an AI agent at Brasspage.

# Changelog

This Actor's version history is a separate document: https://apify.com/brasspage/bulk-ocr-google-sheets-csv/changelog.md

# Actor input Schema

## `sheetUrl` (type: `string`):

Paste a Google Sheets link or a link to a CSV file. The first row must hold the column names, and one column must hold the image or PDF URLs. In Google Sheets, click Share and set General access to 'Anyone with the link' (no sign-in or API key). For a sheet with several tabs, copy the link while the tab is open. Google Drive and Dropbox links to a CSV work too.

## `csvFile` (type: `string`):

A CSV file from your computer (comma, semicolon or tab separated). Used when the link above is empty. Excel files: save as CSV first.

## `datasetId` (type: `string`):

One of your datasets, for example a scraper's output with image URLs. Each item is a row. When set, the link and the file above are not used.

## `urlColumn` (type: `string`):

Optional. The name of the column that holds the image or PDF URLs (case does not matter). Leave empty to use the column with the most links.

## `pages` (type: `string`):

Optional. Pages to read in each file, for example 1 for the first page only, or 1-3, 5. Leave empty for all pages. An image is page 1.

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

The language of scanned pages and images. English also reads most Latin-alphabet text, but accents come out better with the right language. Pages with a text layer are read as they are, in any language.

## `outputFormat` (type: `string`):

Plain text has no formatting. Markdown keeps headings, lists and tables. Tables are pipe tables in both.

## `maxPagesPerRun` (type: `integer`):

Safety limit. The run stops after this many pages in total. The rows after that get 'Skipped' in ocrError.

## Actor input object example

```json
{
  "sheetUrl": "https://docs.google.com/spreadsheets/d/18mhdjzcLFi8WviddH6_IcBbfjPAIfj3tMX4XclA4fOw/edit",
  "urlColumn": "image_url",
  "pages": "1",
  "language": "eng",
  "outputFormat": "text",
  "maxPagesPerRun": 5000
}
```

# Actor output Schema

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

One item per input row, in the same order: every input column, then ocrText, ocrPageCount, ocrPages, ocrTextLayerPages, ocrConfidence, ocrWarnings, ocrFileUrl and ocrError. Export as CSV, Excel or JSON.

## `files` (type: `string`):

Key-value store with one row-NNNNN.txt file for each row whose text is longer than 30,000 characters.

# 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 = {
    "sheetUrl": "https://brasspage.com/examples/bulk-ocr-sample.csv"
};

// Run the Actor and wait for it to finish
const run = await client.actor("brasspage/bulk-ocr-google-sheets-csv").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 = { "sheetUrl": "https://brasspage.com/examples/bulk-ocr-sample.csv" }

# Run the Actor and wait for it to finish
run = client.actor("brasspage/bulk-ocr-google-sheets-csv").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 '{
  "sheetUrl": "https://brasspage.com/examples/bulk-ocr-sample.csv"
}' |
apify call brasspage/bulk-ocr-google-sheets-csv --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,brasspage/bulk-ocr-google-sheets-csv"
        }
    }
}
```

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/Hh1apLSQiAiNJO42b/builds/Iid8WWn4co9WEaa31/openapi.json
