API Doc

One endpoint, an API key, and a JSON response you can use directly.

Authentication

Every request carries an API key. Create one in your dashboard. Keys are shown once and stored hashed, so if you lose one, rotate it rather than trying to recover it.

Authorization: Bearer sk_live_...
# or
X-API-Key: sk_live_...

sk_test_ keys behave identically and are metered the same way; the prefix exists so a key pasted into a log or a ticket is identifiable at a glance.

POST /api/v1/ocr

Send a PDF or an image. Returns typed blocks with pixel-space bounding boxes, parsed tables, and markdown.

curl -X POST https://ocr.run/api/v1/ocr \
  -H "Authorization: Bearer sk_live_..." \
  -F "file=@invoice.pdf"

JSON is accepted too, with either image_base64 (a bare payload or a full data URL) or image_url.

{
  "id": "ocr_3fa2b1c8d94e4f2a9b7c1d3e",
  "pages": 2,
  "duration_ms": 1304,
  "truncated": false,
  "blocks": [
    {
        "type": "title",
        "text": "ACME CORPORATION",
        "bbox": { "x": 74, "y": 91, "width": 515, "height": 49 },
        "bboxNormalised": { "x1": 60, "y1": 52, "x2": 475, "y2": 80 },
        "page": 1
    },
    {
        "type": "table",
        "text": "Item\tQty\nWidget Pro\t10",
        "html": "<table><tr><td>Item</td>...</table>",
        "rows": [["Item", "Qty"], ["Widget Pro", "10"]],
        "bbox": { "x": 76, "y": 428, "width": 1086, "height": 207 },
        "page": 1
    }
  ],
  "markdown": "## ACME CORPORATION\n\n| Item | Qty |\n| --- | --- |...",
  "usage": { "units": 1, "remaining_today": 486 }
}

Block types

titletexttablelistformulaimageheaderfootercaptionunknown

unknown is a real value, not an error: if the model learns a new label, you get the text with a vague type rather than a failed request.

Coordinates

bbox is in pixels against the page as rendered, with a top-left origin. Draw it directly. bboxNormalised keeps the model’s own 0–999 space so you can re-derive positions against a different rendering without re-running the job.

POST /api/v1/ocr/stream

The same parse as server-sent events. First text arrives in roughly a third of a second.

data: {"type":"meta","id":"ocr_...","pageCount":2}
data: {"type":"page","page":1}
data: {"type":"delta","delta":"<|det|>","page":1}
data: {"type":"delta","delta":"title","page":1}
...
data: {"type":"result","result":{ ...same shape as above... }}
data: [DONE]

Deltas split mid-token: a single bounding box arrives across roughly eight of them, so accumulate the text for a live view and wait for the single result event for structure.

GET /api/v1/me and /api/v1/usage

Check your headroom before submitting a batch, rather than discovering the limit through a 429 halfway through.

curl https://ocr.run/api/v1/me -H "Authorization: Bearer sk_live_..."
curl "https://ocr.run/api/v1/usage?days=30" -H "Authorization: Bearer sk_live_..."

Limits

PlanCredits/dayMax filePages/runKeysRate
Free2005 MB1001/page + 1/100 tok
Starter5,00025 MB5031/page + 1/100 tok
Pro50,00050 MB200101/page + 1/100 tok
Business1,000,000100 MB500501/page + 1/100 tok

Every response carries RateLimit-* and X-Quota-* headers. Daily counters reset at 00:00 UTC.

What a run costs

A run is priced per page, in two parts. On the Free plan that is 1 credit per page, plus 1 credit per 100 output tokens, so a one-page receipt costs about 4 credits. A page of dense tables costs more than a page of prose, because it is more work to read. Other plans may charge a different rate, and the table above carries each one.

The per-page part is charged when the document is counted, before the first page is read. That is why a document you cannot afford is refused with quota_exceeded up front rather than part way through. The text part is charged as the model produces it, block by block.

If the allowance runs out mid-document the run stops there. On the streaming endpoint you keep the text that had already arrived and the last event is quota_exceeded; on /api/v1/ocr the answer is a 429 and no partial document. Either way the model is stopped rather than left running, and the pages it never reached are credited back.

Stop reading the response and the run stops: you pay for the pages the model was asked for, including the one in flight, which has already been rendered and sent. The pages it never reached are credited back within a few seconds. A run that fails outright costs nothing.

Errors

Every response, success or failure, carries X-Request-Id. Quote it and a support question has an answer without reproducing anything.

StatuserrorMeaning
400no_inputNo file in the request
401unauthorizedMissing, malformed or revoked key
413file_too_largeAbove your plan's file ceiling
413too_many_pagesAbove your plan's page ceiling
415unsupported_mediaNot a PDF or supported image
429rate_limitedToo many requests per minute
429quota_exceededAllowance spent, or too small for this document
502empty_outputThe model returned nothing
503upstream_unavailableThe model is unreachable
504upstream_timeoutThe model took too long

A run that fails outright is not charged: whatever was reserved for it is credited back. A run that fails after delivering some pages is charged for those pages only. See what a run costs.