Skip to content

Docs

One endpoint turns a PDF or image into markdown, one page per model call, every page at once. Get an API key from the dashboard, then send a document.

Quickstart

curl https://www.opendocrouter.ai/v1/parse \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "google/gemini-3.8-flash-low",
    "document": { "url": "https://arxiv.org/pdf/1706.03762" }
  }'

Check the response's status: a partial result is missing pages. The Python and TypeScript examples retry failed pages once and stop if any still fail, rather than returning an incomplete document.

Request

POST /v1/parse with the key in Authorization: Bearer <key>.

modelRequired. A model ID from the models page.
document{ "url": "https://..." }, { "data": "<base64>", "mime_type": "application/pdf" | "image/png" | "image/jpeg" }, or { "upload_id": "..." } from POST /v1/uploads.
pagesOptional, 1-based: "1-3,7". All pages when left out.
cacheOptional, default false. When true, pages that work are kept for 24 hours, encrypted, and the same page of the same document with the same model comes back free, marked cached. When false, nothing is kept.

Response

Up to 50 pages, always 200 once pages ran, with a status per page. completed means every page worked, partial means some did, and failed means none did. A partial response is an incomplete document: check each page's status before using it. There's no joined markdown; join pages[].markdown yourself.

{
  "id": "0f433d25-c2d5-40df-960f-1cb4c5ddc415",
  "status": "partial",
  "model": "google/gemini-3.8-flash-low",
  "model_version": "2026-09-25",
  "price_version": "2026-09-25",
  "pages": [
    { "page": 1, "status": "ok", "markdown": "# Attention Is All You Need ...", "cached": false,
      "usage": { "input_tokens": 757, "output_tokens": 811 }, "charge_usd": 0.00397 },
    { "page": 2, "status": "error",
      "error": { "code": "timeout", "message": "The page didn't finish before the request deadline" },
      "charge_usd": 0 }
  ],
  "usage": { "input_tokens": 757, "output_tokens": 811 },
  "charge_usd": 0.00397
}

Every response carries an X-Request-Id header, which also appears in your usage log. GET /v1/requests/<id> returns what happened to a request without its markdown: its status, the hold and whether it was released, the charge, and each page's status, error, tokens and charge.

Large documents

A request over 50 pages (up to 500) runs as a job. It returns 202 with the usual response shape, status: "processing", no pages yet and a poll_url. GET /v1/parse/<id> returns the same shape with pages_done while it runs, then the final status and results, up to 100 pages at a time. When has_more is true, fetch the next pages with ?cursor=<next_cursor>.

Files over about 3 MB can go by URL, or by upload: POST /v1/uploads returns a URL to PUT the file to, valid for an hour, and each upload is used once and then deleted.

# 1. Get a one-time upload URL, and PUT the file to it (up to 50 MB)
curl -X POST https://www.opendocrouter.ai/v1/uploads -H "Authorization: Bearer $API_KEY"
# { "upload_id": "9b2c...", "upload_url": "https://...", "max_bytes": 52428800, ... }
curl -T report.pdf "<upload_url>"

# 2. Parse it. Over 50 pages, the answer is 202 with status "processing"
curl https://www.opendocrouter.ai/v1/parse \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "model": "google/gemini-3.8-flash-low", "document": { "upload_id": "9b2c..." } }'

# 3. Poll until status isn't "processing", then page through the results
curl "https://www.opendocrouter.ai/v1/parse/<id>" -H "Authorization: Bearer $API_KEY"
curl "https://www.opendocrouter.ai/v1/parse/<id>?cursor=<next_cursor>" -H "Authorization: Bearer $API_KEY"

Job results are kept encrypted for 24 hours after the job ends, then deleted. DELETE /v1/parse/<id> deletes them now; on a running job it also stops the pages that haven't started, and you're charged only for pages that ran.

Retrying pages

Each page is retried once for you on timeouts, rate limits, provider errors and capacity, and on answers that loop or aren't a transcription. A page error has a code, a message, and a reason when the provider gave one, such as RECITATION, SAFETY, refusal, max_tokens or repetition. If a page still fails, send the request again with just those pages, as the quickstart does:

failed = [n for n, page in pages.items() if page["status"] == "error"]
if failed:
    retry = parse({**body, "pages": ",".join(map(str, failed))})
    pages.update({page["page"]: page for page in retry["pages"]})
Page errorMeaningWorth retrying
timeoutStill running at the deadline, or the provider timed outYes
rate_limitedThe provider rate-limited usYes
provider_errorThe provider returned an error; its message is includedYes
at_capacityNo capacity came free within 10 secondsYes
output_truncatedThe page hit the model's output limitRarely helps
content_filteredThe provider blocked the output, or the model declined (reason says which)Rarely helps
repetitive_outputThe model got stuck repeating text, twiceSometimes
invalid_outputThe model answered without transcribing the page, twiceSometimes
empty_outputThe model returned no textSometimes
response_too_largeThe page didn't fit in the 4.5 MB responseYes, on its own
unreadable_pageThe PDF opened, but this page couldn't be readNo
not_processedThe job ended (it failed or was deleted) before this page ranYes

Results of requests up to 50 pages aren't stored. If your connection drops after one finished, it was still charged, and a retry is a new, charged request.

Errors

A request that can't run returns { "error": { "code", "message" } } and is never charged.

StatusCodeWhen
400invalid_requestBad JSON, unknown model, or a bad pages value
400url_not_allowedThe URL isn't public HTTPS on the default port, or redirects somewhere that isn't
401unauthorizedMissing, unknown or revoked API key
402insufficient_creditsAvailable credit is below the most this request could cost. Includes required_usd and available_usd
403account_pausedThe account is paused; contact us
404not_foundNo such request, job or upload on this account, or the upload was used
410goneA job's results were deleted, by you or 24 hours after it finished
413too_largeOver 500 pages, a request body over 4 MB, or a file over 50 MB
415unsupported_typeNot a PDF, PNG or JPEG
422unreadable_documentAn encrypted or corrupt PDF, or a URL that couldn't be fetched
429rate_limitedMore than 10 requests, or 5 jobs over 50 pages, running at once on the account; or the provider rate-limited every page (never charged). Includes Retry-After
503at_capacityThe model is at capacity or paused. Includes Retry-After
503model_startingA model we host couldn't start in time. Retry after Retry-After (a few minutes)

Billing

Credit is prepaid: top up from $10 in the dashboard. Each page is charged for the tokens it used at the model's price, which includes a small premium over the provider's price. Failed pages, blank pages and cached pages are free; a blank page comes back as ok with empty markdown.

To start, a request needs the most it could possibly cost available (the model's most-per-page price times its pages). That amount is held while it runs; when it finishes, you're charged what it used and the rest is released.

Limits

PagesUp to 50 per request answered directly; 51 to 500 run as a job.
Inline filesRequest body up to 4 MB (a file of about 3 MB).
Files by URLUp to 50 MB, public HTTPS only.
UploadsUp to 50 MB, used within an hour.
Concurrency10 requests and 5 jobs running at once per account. An account's pages use at most a quarter of a model's capacity at once; pages past that wait for the account's earlier pages.
TimePages still running after 270 seconds come back as timeout. A job waits up to 30 minutes for provider capacity.