POST /v1/ocr
Extracts text from a document as markdown. Billed per page processed rather than per token, so no token count applies.
Accepted: PDF, images (jpg, png, heic, webp, gif, avif, tiff) and office documents (pptx, docx, xlsx). An image is read as one page, and a multi page tiff as one page each. Anything else is refused with unsupported_format rather than returning an empty result.
Example
curl -X POST https://relayon.app/v1/ocr \
-H "Authorization: Bearer ro_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "ro-ocr-latest",
"document": {"type": "document_url", "url": "https://example.com/report.pdf"}
}'Three ways to send a document
document.type is document_url for a PDF orimage_url for a png, jpg, webp or gif. The document itself can arrive as a public URL, as base64 in the same field, or as the id of something you uploaded to /v1/files first.
"document": {"type": "document_url", "url": "https://example.com/report.pdf"}
"document": {"type": "document_url", "url": "data:application/pdf;base64,JVBERi0x..."}
"document": {"type": "file_id", "file_id": "file_8f96360be4d2a854b97d06e6b3c8142e"}Inline base64 is capped at 20MB; anything larger belongs in an upload.
Reading part of a document
pages takes zero indexed page numbers and reads only those, for PDFs. You are billed for the pages you select, not for the document they came from, so pulling three pages out of a 248 page textbook costs three pages. A page number past the end fails rather than being quietly skipped.
{
"model": "ro-ocr-latest",
"document": {"type": "file_id", "file_id": "file_59e7b8060cbcb8ded18eb3a9035db5ba"},
"pages": [0, 1, 2]
}table_format and include_images are optional and not every engine can honour them; one that cannot rejects the request rather than ignoring it silently and billing you for a result you did not ask for.
Response
{
"model": "ro-ocr-latest",
"pages": [
{"index": 0, "markdown": "# Quarterly Report\n\n..."}
],
"usage": {"pages_processed": 29}
}usage.pages_processed is what you are billed for. Thepages array is what the engine could return, and the two can differ: an engine that reads a whole document at once returns a single entry carrying all of it while still reporting the true page count. A page selection comes back the same way, as one entry holding the pages you asked for rather than one entry each.
Error codes
Detailed provider failures are available to admins in Diagnostics. The API returns RelayOn error codes and messages.
| Parameter | Type | Default | Description |
|---|---|---|---|
| encrypted_pdf | 422 | — | Encrypted PDFs are unsupported. Upload an unencrypted PDF. |
| no_text_extracted | 500 | — | OCR could not find readable text in the document |
| internal_error | 500 | — | RelayOn could not complete the request; provider details are masked |
| upstream_unavailable | 503 | — | The generation service is temporarily unavailable |
| upstream_connection_error | 502 | — | The connection to the generation service failed |
| upstream_timeout | 504 | — | The generation service did not respond before the deadline |
| no_capacity | 503 | — | No OCR account slot is available right now; honour Retry-After |
| state_unavailable | 503 | — | Request capacity is temporarily unavailable; honour Retry-After |
| unsupported_format / unprocessable_document | 422 | — | Upload a supported, valid PDF or image |
| too_many_pages | 422 | — | Split the document into smaller files |
| invalid_document | 400 | — | The document input is malformed |
| document_too_large | 413 / 422 | — | Over the size limit |
| insufficient_credits | 402 | — | Balance too small for the estimate |
| guardrail_violation | 403 | — | Account or workspace inference policy blocks the effective request |
| invalid_controls | 400 | — | Invalid guardrail or routing settings |
| budget_exceeded | 402 | — | Account, workspace or API key spending limit cannot cover the estimate |
Encrypted PDFs are rejected before document upload to the OCR engine, including PDFs that open without a password but still use permission encryption. Uploading a file successfully only stores it; OCR validation happens when it is processed. This rejection is not retried, billed, or counted against service uptime.
{
"error": {
"message": "Encrypted PDFs are not supported. Please upload an unencrypted PDF.",
"type": "invalid_request_error",
"param": "document",
"code": "encrypted_pdf"
}
}Limits
A document may be up to 512MB and 1,000 pages, however it arrives. A file over the size limit is refused with 413 before anything is sent for processing; one over the page limit is refused once the page count is known, and is not billed either way. Check the selected model’s metadata for any additional limits and supported extraction options.
Large documents: submit a job
A long document can take longer than an HTTP request should be held open, so pass async: true and the request returns 202 with a job to poll. Nothing about the document changes: all three shapes work either way.
curl -X POST https://relayon.app/v1/ocr \
-H "Authorization: Bearer ro_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "ro-ocr-latest",
"async": true,
"document": {"type": "file_id", "file_id": "file_8f96360be4d2a854b97d06e6b3c8142e"}
}'
# 202 Accepted
{"id": "ocrjob_4964f4174e0e248a5cc043e7f3b21ac5", "object": "ocr.job", "status": "queued", ...}
curl https://relayon.app/v1/ocr/jobs/ocrjob_4964f4174e0e248a5cc043e7f3b21ac5 \
-H "Authorization: Bearer ro_live_YOUR_KEY"status moves through queued, running and then succeeded or failed. A succeeded job carries the same result body the synchronous call returns; a failed one carries an error with the code, including encrypted_pdf, and is not billed. Polling a failed job returns HTTP 200; check status and error. A result is kept for 7 days.
Billing
Credits are held against an estimate from the document size before the request runs, then settled at the exact page count. A balance too small for the estimate returns 402 and nothing is sent for processing.