RectoOCR
API reference — v1

Everything the API does,
on one page.

Authentication

Every request carries an API key as a bearer token. Create keys on the dashboard; the secret is shown once and stored only as a SHA-256 hash, so it cannot be recovered later.

Authorization: Bearer rk_live_3f9a2c8b…

A missing key returns 401 missing_key; an unknown or revoked one returns 401 invalid_key. Keys are not scoped — any key can call any endpoint your plan allows.

POST /v1/ocr

POST https://api.recto.dev/v1/ocr

Send one image, get one transcription. The call is synchronous: there is no job id to poll and no webhook to configure.

Request

Either a multipart form with the image under the field file, or the raw image bytes as the request body. Options may be passed as query parameters or as multipart fields.

ParameterDefaultNotes
fileThe image. Multipart only; omit when posting raw bytes.
engineautoauto, fast or precise. See below.
langengTesseract language code, fast engine only. Multiple as eng+deu.
fields0Set to 1 to infer form fields — label/value pairs and ticked boxes — from the page layout.

Accepted formats: PDF, PNG, JPEG, HEIC/HEIF, GIF, WebP, TIFF and BMP.

A PDF is rasterised at 200 dpi and every page is recognised through the same engine, so the response shape never depends on what you sent. Each page costs one page of quota on auto and fast, and twenty on precise. A document longer than your plan's per-request limit is rejected with 402 document_too_long before any of it is charged, so you are never billed for a partial job.

HEIC is transcoded server-side, and EXIF orientation is baked into the pixels before recognition. Phone photos therefore come back upright, and width, height and every word box are in that upright space — the same space the image occupies when a browser displays it.

curl -X POST https://api.recto.dev/v1/ocr \
  -H "Authorization: Bearer $RECTO_KEY" \
  -F "file=@receipt.png" \
  -F "engine=fast"

Response

200 OK with the transcription under result. The shape is the same for both engines; fields an engine cannot produce come back empty rather than missing.

{
  "id": 4217,
  "pages_billed": 2,
  "page_multiplier": 1,
  "pages_used": 38,
  "quota": 5000,
  "result": {
    "engine": "fast",
    "source": "pdf",
    "page_count": 2,
    "text": "STATEMENT\nAshgrove Analytics Ltd…",
    "markdown": "",
    "word_count": 191,
    "mean_conf": 93.4,
    "duration_ms": 940,
    "pages": [
      {
        "page": 1,
        "width": 1700, "height": 2200,
        "text": "STATEMENT\nAshgrove Analytics Ltd…",
        "word_count": 138,
        "mean_conf": 94.1,
        "words": [
          { "text": "STATEMENT", "x": 148, "y": 96,
            "w": 286, "h": 37, "conf": 96.1,
            "block": 1, "line": 1 }
        ]
      }
    ]
  }
}

result.pages is always an array, even for a single image — one shape for every input, so no client needs to branch. Document-level totals sit on result; per-page geometry sits on each entry.

FieldNotes
sourceimage or pdf.
page_countPages recognised, and the number billed.
textThe whole document in reading order, pages separated by a blank line.
markdownLayout-aware Markdown. Precise engine only; empty string on fast.
mean_confWord-weighted mean across pages, so a sparse page cannot skew it.
pages[].words[]Word geometry in that page's pixels. Fast engine only; empty array on precise.
pages[].confPer-word confidence, 0–100. Words tesseract rejected outright are dropped.
pages[].block, lineGrouping indices — words sharing both are on the same rendered line.
pages[].width, heightThat page's raster size, so boxes can be scaled to any display size.

Two response headers report metering on every successful call: X-Recto-Pages-Used and X-Recto-Pages-Quota.

Engines

fast runs Tesseract 5. It is the right default: fast enough to sit in a request path, and the only engine that returns geometry.

auto is the default. For a PDF that already contains its text there is no reason to photograph it and read it back. If you need per-word boxes on every page, ask for engine=fast explicitly — that is the escape hatch, and it is the only thing that changed for existing callers.

auto is for PDFs. It inspects each page in about a millisecond and reads the ones that already carry a text layer directly, sending only the rest to OCR. On a text-based PDF that is roughly 50× faster and character-exact — no recognition, so no recognition errors. The response includes a routing block saying which pages went which way.

The trade is geometry: a text layer is stored as styled runs, not words, so those pages return roughly half as many boxes and one box may span a whole phrase. They are marked "source": "text" and "word_level": false so you never have to guess. Pages that went through OCR in the same document keep per-word boxes and are marked "source": "ocr". Non-PDF input falls back to fast.

precise runs a local vision model. It costs seconds instead of milliseconds and returns no coordinates, but it reads handwriting, skewed phone photos and dense multi-column layouts that classical OCR turns into noise — and it returns structure as Markdown, including tables.

The precise engine requires the Pro plan or above; asking for it on Free returns 402 plan_required.

It also costs more allowance: one precise page draws 20, where auto and fast draw one. The rate is returned as page_multiplier on every response, and pages_billed is already multiplied — result.page_count is always the number of pages actually read. A request that would exceed the remaining quota is refused with 402 quota_exceeded before the model is called.

Working with boxes

Coordinates are in that page's pixels with the origin at the top left. To draw over a scaled display, multiply by displayWidth / page.width. Each page carries its own dimensions, because pages in a PDF need not be the same size.

// black out anything that looks like a card number
const page = result.pages[0];
const scale = canvas.width / page.width;

for (const w of page.words) {
  if (/^\d{4}$/.test(w.text) && w.conf > 80) {
    ctx.fillRect(w.x * scale, w.y * scale, w.w * scale, w.h * scale);
  }
}

Words arrive in reading order, so grouping by block then line reconstructs paragraphs without any sorting of your own.

Errors

Every failure returns the same envelope, with a stable code worth branching on:

{ "error": { "code": "quota_exceeded",
            "message": "Monthly quota of 500 pages used. Upgrade to continue." } }
StatusCodeMeaning
400bad_imageNot a decodable image, or over your plan's size limit.
400unknown_engineengine was neither fast nor precise.
401missing_keyNo Authorization header.
401invalid_keyKey unknown or revoked.
402plan_requiredThe engine is not on your plan.
402quota_exceededThe request needs more pages than the quota has left (precise draws 20 a page).
402document_too_longPDF exceeds your plan's pages-per-request limit.
422ocr_failedReadable request, but recognition failed. Not billed.
429rate_limitedPer-minute rate exceeded. Honour Retry-After.
503engine_unavailablePrecise engine temporarily down. Retry or fall back to fast.

Limits

PlanPages / moReq / minMax filePages / PDFKeys
Loading…

Quotas reset at 00:00 UTC on the first of each month and do not roll over. An image costs one page; a PDF costs one per page. Failed recognitions are recorded in your activity log but never counted against the quota.

Health

GET /v1/health

Unauthenticated. Reports which engines are currently able to serve requests.

{ "status": "ok",
  "engines": { "fast": true, "precise": true } }