POST /v1/ocr returns the text of a PDF or an image, page by page, in Markdown. A page that already has a text layer (a PDF exported from a word processor) is read directly; a scanned page or a photo is read by optical character recognition.
POST https://api.realmoon.ai/v1/ocr- Authentication: an
sk-moon-…key with theinferencescope (see Authentication). - Body:
multipart/(with theform-data Content-Lengthheader) or JSON, in the shape of the Mistral OCR API. - Response: the text of each page read.
The only model is moon-ocr-1. It runs on our servers in France: the document is not sent to any provider and is not kept once the response is returned.
ParametersLink to the Parameters section
MultipartLink to the Multipart section
| Field | Type | Required | Description |
|---|---|---|---|
file (or document) | file | yes | The PDF or the image (PNG, JPEG, WebP, GIF), 30 MB at most. A single file. |
model | string | no | moon-ocr-1 (default). |
pages | string | no | The pages to read, numbered from 0 and separated by commas: 0,2,5. |
JSONLink to the JSON section
| Parameter | Type | Required | Description |
|---|---|---|---|
document | object or string | yes | {"type": or {"type": . A bare string (data: URL or base64) is also accepted. |
model | string | no | moon-ocr-1 (default). |
pages | array of integers | no | The pages to read, starting from 0. |
include_image_base64 | boolean | no | Only false is accepted: page images are not returned. |
id | string | no | Accepted, with no effect. |
In JSON, any other parameter is rejected with 400 .
PagesLink to the Pages section
- Without
pages, the whole document is read, up to 50 pages. Beyond that, the API responds with400: choose the pages, and split a long document across several requests.too_many_pages - An image has a single page, page 0.
- A page requested more than once is read only once.
ExampleLink to the Example section
The OpenAI SDKs have no method for this endpoint: call it over HTTP.
curl https://api.realmoon.ai/v1/ocr \
-H "Authorization: Bearer $MOONAI_API_KEY" \
-F model=moon-ocr-1 \
-F file=@invoice.pdf \
-F pages=0,1import os
import requests
with open("invoice.pdf", "rb") as document:
response = requests.post(
"https://api.realmoon.ai/v1/ocr",
headers={"Authorization": f"Bearer {os.environ['MOONAI_API_KEY']}"},
data={"model": "moon-ocr-1", "pages": "0,1"},
files={"file": ("invoice.pdf", document, "application/pdf")},
timeout=100,
)
response.raise_for_status()
for page in response.json()["pages"]:
print(page["index"], page["source"])
print(page["markdown"])import fs from 'node:fs';
const form = new FormData();
form.append('model', 'moon-ocr-1');
form.append('pages', '0,1');
form.append('file', new Blob([fs.readFileSync('invoice.pdf')], { type: 'application/pdf' }), 'invoice.pdf');
const response = await fetch('https://api.realmoon.ai/v1/ocr', {
method: 'POST',
headers: { Authorization: `Bearer ${process.env.MOONAI_API_KEY}` },
body: form,
});
const result = await response.json();
for (const page of result.pages) console.log(page.index, page.source, page.markdown);The same call in JSON, with the document in base64:
curl https://api.realmoon.ai/v1/ocr \
-H "Authorization: Bearer $MOONAI_API_KEY" \
-H "Content-Type: application/json" \
-d "{\"model\": \"moon-ocr-1\", \"document\": {\"type\": \"document_url\", \"document_url\": \"data:application/pdf;base64,$(base64 -w0 invoice.pdf)\"}, \"pages\": [0, 1]}"ResponseLink to the Response section
{
"model": "moon-ocr-1",
"pages": [
{
"index": 0,
"markdown": "# Invoice No. 2026-118\n\nDate: November 4, 2026\n\n| Item | Amount |\n| --- | --- |\n| Services | €1,200.00 |",
"source": "text",
"images": [],
"dimensions": null
},
{
"index": 1,
"markdown": "Payment terms: 30 days.",
"source": "ocr",
"images": [],
"dimensions": null
}
],
"usage_info": { "pages_processed": 2, "doc_size_bytes": 184320 },
"usage": { "pages": 2, "pages_not_read": 0, "credits": 0 }
}The credits value is made up.
| Field | Type | Description |
|---|---|---|
model | string | Always moon-ocr-1. |
pages[].index | integer | The page number, starting from 0. |
pages[].markdown | string | The page’s text, in Markdown. |
pages[].source | string | text: read from the PDF’s text layer. ocr: read by character recognition. |
pages[].images, pages[].dimensions | array, null | Always [] and null, kept for compatibility with the Mistral shape. |
usage_info.pages_processed, usage_info.doc_size_bytes | integer | Pages read and document size in bytes, as in the Mistral API. |
usage.pages | integer | Pages read and billed. |
usage.pages_not_read | integer | Requested pages the request ran out of time to read. They are neither returned nor billed. |
usage.credits | integer | Credits charged. |
| Header | Value |
|---|---|
X-Moon-Hosting | fr: document read on our servers in France. |
X-Moon-Blur | not_needed: the document never leaves our servers, so Moon Blur is not needed. |
X-Moon-Request-Id, X-Moon-Prompt-Storage | As for any request. |
CostsLink to the Costs section
The price is fixed per page read; it appears in GET / (pricing.credits_per_unit, billing: ) and in the “Models” tab of the portal. The request holds the price of the requested pages, then charges only for the pages actually read. An unreadable document or a request that fails costs nothing.
LimitsLink to the Limits section
| Limit | Value |
|---|---|
| Document size | 30 MB |
| Pages per request | 50 |
| Request duration | About 85 seconds; pages not read in time are neither returned nor billed |
| Requests in flight per workspace | 2 |
The reading service is shared: when other requests keep it busy, the API responds with 429 or 503 ocr_busy. For a large document, send a few pages at a time and run the requests one after another.
ErrorsLink to the Errors section
| HTTP | code | Meaning |
|---|---|---|
| 400 | missing_required_parameter | The file or document is missing. |
| 400 | unknown_parameter | Unknown parameter (in JSON). |
| 400 | unsupported_parameter | include_image_base64 set to true. |
| 400 | url_not_supported | Document given as an https:// URL. |
| 400 | unsupported_format | Neither a PDF nor a PNG, JPEG, WebP or GIF image. |
| 400 | invalid_image | Unreadable image, or more than 40 megapixels. |
| 400 | encrypted_document | Password-protected PDF. |
| 400 | invalid_document | PDF with no pages. |
| 400 | too_many_pages | More than 50 pages without pages. |
| 400 | null | A page number that does not exist, a page other than 0 for an image, an empty document, invalid base64 (param names it). |
| 403 | org_policy | Your organization’s rules refuse the API or this model. |
| 404 | model_not_found | A model other than moon-ocr-1. |
| 411 | length_required | Multipart upload without a Content-Length header. |
| 413 | file_too_large, request_too_large | Document larger than 30 MB. |
| 422 | unreadable_document | The document is unreadable or too heavy to read. |
| 429 | too_many_concurrent | Too many OCR requests in flight for this workspace, or for the service. |
| 503 | ocr_busy | The reading service is busy: retry after Retry-After (10 seconds). |
| 504 | ocr_timeout | Reading took too long: send fewer pages. |
Key, balance and spend cap errors are in Errors.