Read documents with OCR
The OCR endpoint reads PDFs and images and returns their text as Markdown or structured layout blocks. Billing is per page, charged before reading and refunded automatically on failure.
POST /v1/ocr reads a PDF, PNG, JPEG, or WebP file and returns its text. Send the file as base64 in document with the model id paxa-ocr-lite-v1, and the response carries one entry per page. Like every endpoint, the request is charged before inference and refunded automatically when inference fails (Credits).
# Encode without line wrapping: wrapped base64 breaks the JSON string.
DOC=$(base64 < invoice.pdf | tr -d '\n')
# --max-time covers a multi-page document; curl defaults to no limit.
curl -X POST https://api.paxalabs.com/v1/ocr \
--max-time 300 \
-H "Authorization: Bearer $PAXA_API_KEY" \
-H "Content-Type: application/json" \
-d "{\"document\": \"$DOC\", \"model\": \"paxa-ocr-lite-v1\"}"import { readFile } from "node:fs/promises";
const document = (await readFile("invoice.pdf")).toString("base64");
const response = await fetch("https://api.paxalabs.com/v1/ocr", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.PAXA_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ document, model: "paxa-ocr-lite-v1" }),
// A multi-page document can run for minutes; give it room.
signal: AbortSignal.timeout(300_000),
});
if (!response.ok) throw new Error(`OCR failed: ${response.status}`);
const { pages, usage } = await response.json();
console.log(pages[0].markdown, `${usage.credits} credits`);import base64
import os
import requests
with open("invoice.pdf", "rb") as file:
document = base64.b64encode(file.read()).decode()
response = requests.post(
"https://api.paxalabs.com/v1/ocr",
headers={"Authorization": f"Bearer {os.environ['PAXA_API_KEY']}"},
json={"document": document, "model": "paxa-ocr-lite-v1"},
# A multi-page document can run for minutes; give it room.
timeout=300,
)
response.raise_for_status()
body = response.json()
print(body["pages"][0]["markdown"], body["usage"]["credits"], "credits"){
"pages": [
{
"page": 1,
"markdown": "# ใบเสร็จรับเงิน\n\nร้านข้าวแกงบ้านสวน สาขาสีลม\n\n| รายการ | จำนวน | ราคา |\n| --- | --- | --- |\n| ข้าวแกงเขียวหวานไก่ | 1 | 60 |\n| น้ำเปล่า | 1 | 10 |\n\nรวมทั้งสิ้น 70 บาท"
}
],
"usage": {
"pages": 1,
"credits": 6.5
}
}Billing#
A page costs 6.5 credits, whatever is on it. A page is one PDF page or one image file. The page count is read from the document itself before the charge. The cost is known before any reading starts.
The charge is reported only in the body's usage.credits. This endpoint sends no x-credits-charged header. A fast response and a slow one carry the same wire surface. When reading fails after the charge, the refund is automatic and the response is 502 provider_error.
Documents and formats#
document carries the raw file bytes as base64. Four formats are read: PDF, PNG, JPEG, and WebP. The format is detected from the bytes, and an image always counts as one page. A file that cannot be read as one of the four answers 400 document_invalid, and a PDF that needs a password to open answers 400 document_password_required. A PDF that carries permissions-only encryption, the kind that opens without being asked for a password, is read normally. None of this charges.
Base64 grows a payload by about a third. Mind your client's body limits when sending large scans, and remove PDF passwords before encoding.
Page and size limits#
One request reads at most 50 pages, and a longer document answers 400 too_many_pages. The decoded file may be at most 10 MiB, and a larger one answers 413 document_too_large. Neither refusal charges anything. Split larger documents and send the parts as separate requests.
Choose an output#
output selects the result shape and defaults to "markdown". Both shapes carry the same reading. The difference is how much structure travels with it.
Markdown#
The default returns each page as GitHub-flavored Markdown in reading order. Headings, lists, and tables arrive as Markdown structures. The output pastes into wikis, prompts, and search indexes directly. Join pages with a blank line to rebuild the document.
Structured blocks#
Set output to "structured" and each page arrives as typed blocks in reading order. Each block carries its kind and its content in typed fields. A pipeline can route headings, tables, and prose without parsing Markdown.
| Block | Carries | Meaning |
|---|---|---|
| heading | text, level | A section title. level starts at 1 for the most prominent. |
| paragraph | text | A run of body text. |
| list | items | The list entries in order. |
| table | rows | The table cells as rows of column values, first row first. |
| figure | text | An image or chart. text is its caption or nearby label. |
DOC=$(base64 < invoice.pdf | tr -d '\n')
curl -X POST https://api.paxalabs.com/v1/ocr \
--max-time 300 \
-H "Authorization: Bearer $PAXA_API_KEY" \
-H "Content-Type: application/json" \
-d "{\"document\": \"$DOC\", \"model\": \"paxa-ocr-lite-v1\", \"output\": \"structured\"}"{
"pages": [
{
"page": 1,
"blocks": [
{
"type": "heading",
"level": 1,
"text": "ใบเสร็จรับเงิน"
},
{
"type": "paragraph",
"text": "ร้านข้าวแกงบ้านสวน สาขาสีลม"
},
{
"type": "table",
"rows": [
[
"รายการ",
"จำนวน",
"ราคา"
],
[
"ข้าวแกงเขียวหวานไก่",
"1",
"60"
],
[
"น้ำเปล่า",
"1",
"10"
]
]
},
{
"type": "paragraph",
"text": "รวมทั้งสิ้น 70 บาท"
}
]
}
],
"usage": {
"pages": 1,
"credits": 6.5
}
}Reliability and retries#
The response is one buffered JSON body. A multi-page document can take minutes. Set the client timeout generously. The samples above allow 300 seconds.
A request that runs long enough writes its response headers before the outcome is known. Intermediate proxies abandon a connection that has been silent too long, and the early headers prevent that. Such a response always has status 200. It reports a failure in the body, as the same problem document a failed request returns: a title and a status where pages would be. Long documents are the requests that reach it. Treat a body carrying title as the error it describes. Shorter documents always carry their status on the status line.
Retries are safe with an Idempotency-Key. A replay of a delivered request serves the same reading again without a second charge. When reading fails, the charge is refunded and a fresh request needs a new key.