# The OCR-AI API

Read PDFs and images into text, fields and tables, from an MCP client or over plain HTTPS.

Base URL: https://ocr-ai.com. Get a key, with 100 free pages: https://ocr-ai.com/en/agents. Human version of this page: https://ocr-ai.com/en/docs.

## Authentication

Every call that reads or counts pages needs an API key, sent as Authorization: Bearer ocrai_sk_… (or as X-API-Key: ocrai_sk_…).

Keys are created on your account page, after you ask for access on the agents page. A key is shown once, when it is created, and can be revoked there at any time. Keep keys on the server side: never in a web page or a mobile app.

## MCP

Endpoint: /mcp, over the Streamable HTTP transport. The server is stateless: every POST gets one JSON response, with no session and no event stream. Supported protocol versions: 2025-11-25, 2025-06-18, 2025-03-26 and 2024-11-05.

initialize and tools/list work without a key; tools/call needs one. Problems a model can act on (no key, not enough pages, an argument the tool does not accept, a file it cannot read) come back as tool results with isError: true, a sentence for the model, and an error code in structuredContent.

Requests that send no Origin header, and requests from any https origin, are accepted: the API key is the only credential. A page on a null, file: or plain-http origin, or on any other scheme, is refused with 403, at the CORS preflight too.

A call of many pages can take more than a minute. MCP clients stop waiting after a set time (60 seconds by default in the MCP TypeScript SDK): raise it for read_document, or read fewer pages per call. A call whose connection closes before the answer is sent is refunded in full; pages a client stops waiting for while it keeps the connection open are charged.

| Tool | Arguments | Returns |
| --- | --- | --- |
| read_document | file_url or file_base64 (one of them); pages, e.g. "1-5,8"; hint: auto, invoice, receipt, card, table, text (card is a business card); language of labels and summary: en or he | Markdown text, and structuredContent: the document object |
| check_balance | none | balance, expired, buy_url, packs |
| buy_pages | pack: p100, p500, p2000; currency: USD (default) or ILS | buy_url for that pack, with its pages and price |

```json
{
  "mcpServers": {
    "ocr-ai": {
      "type": "http",
      "url": "https://ocr-ai.com/mcp",
      "headers": {
        "Authorization": "Bearer ocrai_sk_…"
      }
    }
  }
}
```

Calling a tool without an MCP client:

```sh
curl https://ocr-ai.com/mcp \
  -H "Authorization: Bearer ocrai_sk_…" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -d '{"jsonrpc": "2.0", "id": 1, "method": "tools/call",
       "params": {"name": "read_document",
                  "arguments": {"file_url": "https://example.com/invoice.pdf"}}}'
```

## REST

### POST /api/v1/read

Send the document in one of three ways:

- multipart/form-data with a file field named file, and optional fields pages, hint and language;
- application/json with file_base64 or file_url (a public https link), and optional pages, hint and language;
- the file itself, with its own Content-Type (application/pdf, image/jpeg, image/png or image/webp), and the options as query parameters.

The answer is the document object. The file's type is taken from its bytes, not from its name or the declared type.

Fields and query parameters are checked: one the read does not know, one of the wrong type, or a hint or language outside its list answers 400 bad_request, and the message names it.

Every check is made before the answer starts. The 200 status is then sent at once, and the body when the read is done. If the read fails after that, the body breaks off instead of arriving as partial JSON, and every page of the call is given back. A caller that disconnects before the body is sent gets its pages back the same way.

```sh
curl https://ocr-ai.com/api/v1/read \
  -H "Authorization: Bearer ocrai_sk_…" \
  -F "file=@invoice.pdf" \
  -F "pages=1-3"
```

```sh
curl https://ocr-ai.com/api/v1/read \
  -H "Authorization: Bearer ocrai_sk_…" \
  -H "Content-Type: application/json" \
  -d '{"file_url": "https://example.com/invoice.pdf", "hint": "invoice"}'
```

```sh
curl "https://ocr-ai.com/api/v1/read?pages=1-3&language=he" \
  -H "Authorization: Bearer ocrai_sk_…" \
  -H "Content-Type: application/pdf" \
  --data-binary @invoice.pdf
```

### GET /api/v1/balance

Pages left on the key's account, whether unused pages have expired, and the packs, each with a buy_url. With language=he in the query string, the links open the Hebrew page and the packs are priced in shekels.

```sh
curl https://ocr-ai.com/api/v1/balance \
  -H "Authorization: Bearer ocrai_sk_…"
```

## The document object

| Field | Meaning |
| --- | --- |
| object | "document" |
| total_pages | Pages in the file. |
| pages_read | Pages read in this call. |
| pages_charged | Pages charged in this call. Failed pages are not charged. |
| balance | Pages left on the account after this call. |
| next_pages | The page range for the next call, when the file has more pages than one call reads; otherwise null. |
| buy_url | Present when the account ran out of pages: where a person can add more. |
| pages[] | One entry per page read, in page order: page, status (done, failed or no_pages) and, for a page that was read, partial, document_type, language, summary, fields, tables and text. |

Each field has key, label, value and type, and when they apply normalized (dates as YYYY-MM-DD, amounts and numbers as plain decimals), box ([ymin, xmin, ymax, xmax], 0–1000 relative to the page) and uncertain (true when the value was hard to read).

Each table has columns and rows (an array of cells per row), and when known a title and a box.

Example response (shortened):

```json
{
  "object": "document",
  "total_pages": 1,
  "pages_read": 1,
  "pages_charged": 1,
  "balance": 99,
  "next_pages": null,
  "pages": [
    {
      "page": 1,
      "status": "done",
      "partial": false,
      "document_type": "invoice",
      "language": "he",
      "summary": "חשבונית מס 20417 מסטודיו תמר לקפה נמל, על סך 6,018.00 ₪ כולל מע״מ.",
      "fields": [
        {
          "key": "document_number",
          "label": "מספר חשבונית",
          "value": "20417",
          "type": "id",
          "box": [
            124,
            743,
            143,
            798
          ]
        },
        {
          "key": "issue_date",
          "label": "תאריך הפקה",
          "value": "21/10/2026",
          "normalized": "2026-10-21",
          "type": "date",
          "box": [
            144,
            706,
            163,
            798
          ]
        },
        {
          "key": "total",
          "label": "סה״כ לתשלום",
          "value": "6,018.00 ₪",
          "normalized": "6018",
          "type": "money",
          "box": [
            595,
            86,
            618,
            202
          ]
        }
      ],
      "tables": [
        {
          "title": "פירוט החשבונית",
          "columns": [
            "#",
            "תיאור",
            "כמות",
            "מחיר יחידה",
            "סה״כ"
          ],
          "rows": [
            [
              "1",
              "עיצוב שלט חזית",
              "1",
              "2,400.00",
              "2,400.00"
            ],
            [
              "2",
              "הדפסה על לוח אלומיניום 120×60",
              "2",
              "680.00",
              "1,360.00"
            ]
          ],
          "box": [
            349,
            73,
            516,
            928
          ]
        }
      ],
      "text": "סטודיו תמר — שילוט ועיצוב גרפי בע״מ\nרח׳ הנגב 12, באר שבע · טל׳ 08-6230145\n…"
    }
  ]
}
```

## ERP and CRM

The keys in fields are fixed: the same field always arrives under the same key, so you map them once to your ERP or CRM fields. The machine value is in normalized: dates as YYYY-MM-DD, amounts as decimals.

A flow in Make, n8n, Zapier or Power Automate: an HTTP step sends the document to POST /api/v1/read, maps fields and tables, and creates the record in your system. A value with "uncertain": true can stop for a person to check.

| Keys | In your system's record |
| --- | --- |
| vendor_name, vendor_tax_id, vendor_address | The vendor, or a supplier account in the ERP. |
| document_number, allocation_number, issue_date, due_date | The document header: number, allocation number and dates. |
| subtotal, discount, vat_rate, vat_amount, total, currency | The document's amounts. |
| tables[0].rows | The line items. |
| full_name, job_title, company, email, phone, mobile, fax, website, address | A contact or lead in the CRM. On a business card, document_type is business_card. |

The workspace also downloads the same data as files: an ERP file and a CRM file with fixed columns, and a vCard. Column lists on the integrations page: https://ocr-ai.com/en/integrations

## Pages and payment

- One page of a PDF, or one image, is one page. A page is charged when it is read; if the read fails, the page is given back and its entry says status: "failed" and refunded: true.
- A call is checked before anything is read. When the account has fewer pages than the call needs, the answer is 402 payment_required, with needed, balance, total_pages, buy_url and packs. Nothing is read and nothing is charged.
- buy_url opens the key's top-up page, where a pack is chosen; each pack's own buy_url, in packs, opens Stripe Checkout for that pack directly. Paying adds the pages to the key's account. It signs nobody in and gives no access to the account, so the links are safe to hand to whoever pays. In a read with language: "he", the page is in Hebrew and the packs are priced in shekels.
- New accounts get 100 free pages with their first key. If the day's free starts have all been given, the pages are added at a read or balance call on a later day. Pages stay valid for 12 months from the last time pages were added to the account, by a purchase or the free start.

A 402 response:

```json
{
  "error": "payment_required",
  "message": "This call reads 4 pages; the key's account has 3. Ask your user to add pages at buy_url, or read fewer pages with \"pages\".",
  "needed": 4,
  "balance": 3,
  "total_pages": 4,
  "buy_url": "https://ocr-ai.com/en/topup?key=key_7Hq2mXc9Lp0a",
  "packs": [
    {
      "pack": "p100",
      "pages": 100,
      "price": 9,
      "currency": "USD",
      "buy_url": "https://ocr-ai.com/api/v1/buy?key=key_7Hq2mXc9Lp0a&pack=p100&currency=USD"
    },
    {
      "pack": "p500",
      "pages": 500,
      "price": 29,
      "currency": "USD",
      "buy_url": "https://ocr-ai.com/api/v1/buy?key=key_7Hq2mXc9Lp0a&pack=p500&currency=USD"
    },
    {
      "pack": "p2000",
      "pages": 2000,
      "price": 89,
      "currency": "USD",
      "buy_url": "https://ocr-ai.com/api/v1/buy?key=key_7Hq2mXc9Lp0a&pack=p2000&currency=USD"
    }
  ]
}
```

## Limits

- Up to 20 pages per call, 20 MB per file, and 28 MB per request body, so a 20 MB file sent as base64 fits.
- A PDF may have up to 5000 pages in all; a longer one is refused whole (422 too_many_pages). A page larger than 8 MB once taken out of its PDF fails and is not charged (page_too_large).
- About 30 calls a minute per key; above that, 429 rate_limited.
- file_url must be a public https link: no user name or password in it, no IP address and no internal host name. It may redirect up to 3 times, and every address on the way is held to the same rule. The download must finish within 20 seconds.
- Pages are read a few at a time, yet a call of many pages can take more than a minute: give the request enough time, or read fewer pages per call.

## Errors

Every error answer has error, a stable code, and message, a sentence a model can pass on to its user. Some add facts: get_key_url on 401, the payment fields on 402, max_pages on too_many_pages.

| HTTP | error | When |
| --- | --- | --- |
| 400 | bad_request | The body could not be read or is not a JSON object, or a field or parameter is unknown, of the wrong type or outside its allowed values. The message names it. |
| 400 | missing_file | No multipart field named file, or neither file_base64 nor file_url. |
| 400 | bad_base64 | file_base64 is not valid base64. |
| 400 | bad_url | file_url is not a public https link, or one of its redirects leads to an address that is not. |
| 400 | bad_pages | pages is malformed, or selects no page of the file. |
| 401 | unauthorized | No key, or a key that is unknown or revoked. The answer has get_key_url and a WWW-Authenticate header. |
| 402 | payment_required | The account has fewer pages than the call needs. Nothing is read or charged. |
| 404 | not_found | No such endpoint under /api/v1. |
| 405 | method_not_allowed | The wrong HTTP method: read is POST, balance is GET. |
| 413 | too_large | The request body is over 28 MB, or the file is over 20 MB. |
| 415 | unsupported_type | The Content-Type is not multipart, JSON or a supported file type, or the file is not a PDF, JPEG, PNG or WebP. |
| 422 | unreadable_pdf | The PDF cannot be opened, counted or split, or its internal structure is too large to open. |
| 422 | too_many_pages | The PDF has more than 5000 pages. |
| 422 | fetch_failed | file_url could not be downloaded: an error status, more than 3 redirects, or more than 20 seconds. |
| 429 | rate_limited | Too many calls in the last minute. |
| 500 | server_error | An unexpected failure before the read started. Nothing was charged. |
| 503 | engine_unavailable | Reading is unavailable right now. Nothing was charged. |

### A page that failed

A call can succeed while some of its pages do not. Every entry in pages[] has a status, and a failed page has an error code as well:

| status / error | Meaning |
| --- | --- |
| done | The page was read and charged. |
| failed | The page was not read, and was given back (refunded: true). error says why: |
| engine_busy | Reading was busy. Try the page again in a minute. |
| engine_refused | This page cannot be read. |
| engine_failed | The read failed. Try again. |
| unreadable_page | The page could not be taken out of its PDF. |
| page_too_large | The page is larger than 8 MB once taken out of its PDF. |
| no_pages | The account ran out of pages during the call: this page was not read or charged, and the answer has buy_url. |

### MCP errors

/mcp answers 200 with a JSON-RPC response, or a batch of them, and 202 with no body when only notifications or responses were posted. Its errors:

| HTTP | JSON-RPC | When |
| --- | --- | --- |
| 400 | -32700 | The body is not JSON. |
| 400 | -32600 | The batch is empty. |
| 400 | -32000 | The MCP-Protocol-Version header names a version this server does not support. |
| 403 | -32000 | The request's Origin is refused. |
| 405 | -32000 | Not a POST. |
| 413 | -32000 | The body is over 28 MB. |
| 500 | -32603 | An internal error before any read started. Nothing was charged. |
| 200 | -32600 | A message that is not a JSON-RPC request: not an object, jsonrpc is not "2.0", method is not a string, or id is not a string or an integer. |
| 200 | -32601 | The method does not exist. |
| 200 | -32602 | params is not an object, or the tool does not exist. |
| 200 | -32603 | An internal error during a read. Every page of that call was given back. |

Inside tools/call, problems a model can act on come back as tool results with isError: true, their code in structuredContent.error: missing_key and invalid_key (with get_key_url), rate_limited, and bad_request for an argument the tool does not accept. read_document adds the codes of the REST read: payment_required (with the payment fields), missing_file, bad_base64, bad_url, bad_pages, too_large, unsupported_type, unreadable_pdf, too_many_pages, fetch_failed and engine_unavailable.
