Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.unsiloed.ai/llms.txt

Use this file to discover all available pages before exploring further.

Overview

The v3 endpoint parses a PDF (or archive of PDFs) and returns markdown text for each page. Compared to v1/v2 it is intentionally simpler: no layout / OCR-engine / segment-analysis knobs, no segment tree in the response. You submit a PDF, you get markdown back. The pipeline picks the best model per page internally. The endpoint is async: every submission returns a job_id you poll until the job reaches a terminal state. Endpoint base URL: https://prod.visionapi.unsiloed.ai/v3/parse
Use this endpoint when you want clean markdown out of a PDF without configuring layout or OCR settings. For fine-grained control over segment types, bounding boxes, and per-segment processing, use the v2 Parse Document endpoint instead.
The v3 surface has four routes:
RouteUse it for
POST /v3/parseSubmit a single PDF — three body shapes (multipart, JSON URL, JSON file_id)
POST /v3/parse/uploadMint a presigned PUT URL for PDFs larger than the inline cap
POST /v3/parse/batchSubmit a tar/tar.gz/zip archive of PDFs in one job
GET /v3/parse/{job_id}Poll status and retrieve the inline markdown result

Authentication

Every request requires an X-API-Key header. Keys are personal, rate-limited per key (100 requests/day, 2 RPS), and isolated — you can only see your own jobs.
v3 API keys are issued on request — they are separate from v1/v2 keys. To get one, email aman@unsiloed.ai and andre@unsiloed.ai (or open an issue at github.com/Unsiloed-AI/unsiloed-olmocr-bench) with a one-line note about what you’re evaluating. Typical turnaround is same-day.

Guarantees

PropertyWhat it means for you
Per-key isolationPolling another user’s job_id returns 404, as does trying to re-parse another user’s file_id.
24-hour retentionEvery job artifact — your uploaded PDF, status, result, container logs — is deleted automatically 24 hours after the job is created. Pull your results within that window.
No scoring on our endThe API returns markdown only. If you want to reproduce a benchmark number, run the unmodified upstream scorer against the markdown locally.

POST /v3/parse — Submit a single PDF

POST /v3/parse accepts three body shapes (auto-detected from the Content-Type header). All three submit the same async job and return the same response.

Body shape 1 — Inline multipart upload

For small PDFs (up to ~3 MB raw). Single HTTP call.
file
file
required
PDF binary, sent as multipart/form-data. Capped at ~3 MB raw (≈ 4 MB after base64 encoding inside API Gateway). For larger files use body shape 2 or 3.
pages
string
Optional query parameter on the request URL. Restrict OCR to a subset of pages.
  • "1-5": pages 1 through 5
  • "1,3,5": specific pages
  • omitted: all pages
curl -X POST \
  -H "X-API-Key: <your-api-key>" \
  -F file=@input.pdf \
  https://prod.visionapi.unsiloed.ai/v3/parse

Body shape 2 — JSON with caller-hosted URL

For PDFs up to 50 MB that you already host (S3 public-read, S3 presigned, GitHub release asset, your own web server, etc.). Single HTTP call.
url
string
required
Publicly fetchable https:// URL or s3://bucket/key reference to the PDF. We fetch it. URLs pointing at private IPs, link-local, or AWS instance metadata are rejected by an SSRF guard.
curl -X POST \
  -H "X-API-Key: <your-api-key>" \
  -H "Content-Type: application/json" \
  -d '{"url":"https://example.com/your-paper.pdf"}' \
  https://prod.visionapi.unsiloed.ai/v3/parse

Body shape 3 — JSON with file_id from a presigned upload

For PDFs up to 50 MB that you do not want to host publicly. First call POST /v3/parse/upload (below) to get a presigned upload_url and file_id; PUT your PDF to the URL; then submit the parse using the file_id.
file_id
string
required
The file_id returned by POST /v3/parse/upload after you finish the PUT. Acts as the job_id for subsequent polling.
curl -X POST \
  -H "X-API-Key: <your-api-key>" \
  -H "Content-Type: application/json" \
  -d '{"file_id":"<file_id>"}' \
  https://prod.visionapi.unsiloed.ai/v3/parse
You must use the same API key that minted the file_id via POST /v3/parse/upload. Cross-key submissions return 404 (hiding existence) — this is how per-key isolation is enforced.

Response (any body shape)

job_id
string
required
Job identifier (32-character UUID hex). Pass to GET /v3/parse/{job_id} to poll. When you used body shape 3, this equals the file_id you submitted.
status
string
required
Always "queued" on submission. Subsequent values: "running""done" or "failed".
created_at
string
required
ISO 8601 timestamp when the job was created.
{
  "job_id":     "5eb3493042f84af5860531df5b18c56b",
  "status":     "queued",
  "created_at": "2026-05-11T18:47:26Z"
}

POST /v3/parse/upload — Presigned upload URL

Returns a presigned S3 PUT URL so you can upload a PDF directly (bypassing the API Gateway request size cap). Use this for the 3-call flow of body shape 3 above. No request body required — just an empty POST with the auth header.
curl -X POST \
  -H "X-API-Key: <your-api-key>" \
  https://prod.visionapi.unsiloed.ai/v3/parse/upload

Response

file_id
string
required
Opaque identifier. After you PUT the PDF to upload_url, pass this back as {"file_id": "..."} to POST /v3/parse to start parsing.
upload_url
string
required
Presigned S3 PUT URL. 1-hour expiry from issuance. Send the PDF body directly to this URL with HTTP method PUT and Content-Type: application/pdf. The transfer bypasses our API Gateway entirely.
upload_method
string
required
Always "PUT".
upload_content_type
string
required
Always "application/pdf". Your PUT must set the same Content-Type header.
max_bytes
integer
required
Maximum PDF size accepted by the pipeline after upload. Currently 52428800 (50 MB).
expires_in
integer
required
Seconds until the upload_url expires (3600).
{
  "file_id":             "527f4097f3d1...",
  "upload_url":          "https://...s3.amazonaws.com/...?X-Amz-Signature=...",
  "upload_method":       "PUT",
  "upload_content_type": "application/pdf",
  "max_bytes":           52428800,
  "expires_in":          3600,
  "next":                "POST /v3/parse with body {\"file_id\": \"527f4097f3d1...\"}"
}

Full 3-call flow

export API=https://prod.visionapi.unsiloed.ai/v3/parse
export KEY=<your-api-key>

# 1. Mint a presigned URL
UP=$(curl -s -X POST -H "X-API-Key: $KEY" $API/upload)
FILE_ID=$(jq -r .file_id <<< "$UP")
UPLOAD_URL=$(jq -r .upload_url <<< "$UP")

# 2. PUT the PDF directly to S3 (no auth header needed — URL is presigned)
curl -X PUT \
  -H "Content-Type: application/pdf" \
  --upload-file big.pdf \
  "$UPLOAD_URL"

# 3. Start the parse
curl -X POST \
  -H "X-API-Key: $KEY" \
  -H "Content-Type: application/json" \
  -d "{\"file_id\":\"$FILE_ID\"}" \
  $API

POST /v3/parse/batch — Archive of PDFs

Process many PDFs in one job. You host an archive of PDFs; we fetch it and process every PDF inside.
url
string
required
Public https:// URL or s3:// reference to a .tar, .tar.gz/.tgz, or .zip archive of PDFs. Archive format is auto-detected by content sniffing the first bytes, not by file extension. Non-PDF files inside the archive are skipped silently.
curl -X POST \
  -H "X-API-Key: <your-api-key>" \
  -H "Content-Type: application/json" \
  -d '{"url":"https://example.com/my-bench.tar.gz"}' \
  https://prod.visionapi.unsiloed.ai/v3/parse/batch
Submission response is the same shape as POST /v3/parse:
{
  "job_id":     "...",
  "status":     "queued",
  "created_at": "..."
}
The completion response uses documents[] instead of pages[] (one entry per PDF in the archive) — see “Polling” below.

GET /v3/parse/ — Poll status + retrieve result

curl -H "X-API-Key: <your-api-key>" \
  https://prod.visionapi.unsiloed.ai/v3/parse/<job_id>

Query parameters

format
string
When set to "markdown" and status is "done", returns concatenated page markdown as Content-Type: text/markdown; charset=utf-8 instead of a JSON envelope. Useful for curl ... | tee out.md. Ignored while the job is queued/running/failed.

Response — while running

{
  "job_id":     "5eb3493042f84af5860531df5b18c56b",
  "status":     "running",
  "created_at": "...",
  "started_at": "...",
  "progress":   { "page": 2, "of": 5 },
  "phase":      "ocr"
}

Response — single-PDF done

status
string
required
"done" for a completed single-PDF job.
page_count
integer
required
Number of pages in the PDF (after applying the pages selector, if any).
pages
array
required
Per-page markdown. Each entry has page (1-indexed integer) and markdown (string). Page order is ascending.
{
  "job_id":      "5eb3493042f84af5860531df5b18c56b",
  "status":      "done",
  "file_name":   "input.pdf",
  "created_at":  "...",
  "started_at":  "...",
  "finished_at": "...",
  "page_count":  3,
  "pages": [
    { "page": 1, "markdown": "..." },
    { "page": 2, "markdown": "..." },
    { "page": 3, "markdown": "..." }
  ]
}

Response — batch done

documents
array
required
One entry per PDF found in the archive. Each entry has pdf (relative path inside the archive), page_count, and pages[] (same shape as single-PDF). If a particular PDF failed, the entry has an error field instead of pages.
{
  "job_id":     "...",
  "status":     "done",
  "source_url": "https://example.com/my-bench.tar.gz",
  "pdf_count":  3,
  "documents": [
    { "pdf": "doc1.pdf", "page_count": 1,
      "pages": [{ "page": 1, "markdown": "..." }] },
    { "pdf": "doc2.pdf", "page_count": 2,
      "pages": [{ "page": 1, "markdown": "..." }, { "page": 2, "markdown": "..." }] },
    { "pdf": "broken.pdf",
      "error": "PdfStreamError: Stream has ended unexpectedly" }
  ]
}
If the JSON would exceed API Gateway’s 10 MB response cap, the response is { "job_id", "status": "done", "result_url" } instead — fetch result_url (presigned S3 GET) to download the same JSON. The schema of the downloaded JSON is identical to the inline shape, so clients can use one code path for both.

Response — failed

{
  "job_id":     "...",
  "status":     "failed",
  "created_at": "...",
  "error":      "PDF exceeds size limit (50 MB)"
}

Polling example

import requests, time

API = "https://prod.visionapi.unsiloed.ai/v3/parse"
KEY = "<your-api-key>"

def wait_for(job_id):
    while True:
        r = requests.get(f"{API}/{job_id}", headers={"X-API-Key": KEY})
        r.raise_for_status()
        body = r.json()
        status = body["status"]
        if status in ("done", "failed"):
            return body
        time.sleep(5)

Error responses

StatusBody / HeaderWhenWhat to do
401{"message": "Unauthorized"}Missing or invalid X-API-KeyUse a personal key; request one if you don’t have it yet
403{"message": "Forbidden"}Key not yet propagated through API Gateway edges (within 30–60s of issuance)Retry after a minute
404{"error": "Job not found"}Job doesn’t exist, or the job belongs to a different API keyConfirm you’re using the same key that submitted the job; otherwise check the job_id
404{"error": "no upload found for file_id=..."}The file_id you sent doesn’t have an uploaded PDF behind it, or it belongs to a different keyMake sure you completed the PUT step from /upload, and that you’re using the same key
413{"message": "Request Too Long"}Multipart body too big for API Gateway’s request capSwitch to body shape 2 (JSON URL) or body shape 3 (presigned upload)
429{"message": "Limit Exceeded"}Per-key quota (100 requests/day) or rate limit (2 RPS / 2 burst) exceededSlow down and retry; quota resets daily
400{"error": "invalid url: ..."}URL validation failed (wrong scheme, IP-literal, link-local, RFC1918, AWS metadata, etc.)Use an https:// or s3:// URL pointing at a public/presigned object
400{"error": "multipart parse failed: ..."}Malformed multipart bodyVerify your client sets Content-Type: multipart/form-data; boundary=... correctly
job failed (200 body){"status": "failed", "error": "..."}Container hit a runtime error (file isn’t a real PDF, archive contains no PDFs, fetch URL timed out, etc.)Read the error field; fix and resubmit

Code examples

curl -X POST \
  -H "X-API-Key: your-api-key" \
  -F file=@input.pdf \
  "https://prod.visionapi.unsiloed.ai/v3/parse"
{
  "job_id":     "5eb3493042f84af5860531df5b18c56b",
  "status":     "queued",
  "created_at": "2026-05-11T18:47:26Z"
}

See also

  • Open-source benchmark harness + client — reproduces our published olmOCR-Bench numbers across vendors and includes a thin client (clients/bench_via_api.py) that calls this endpoint, collects the returned markdown, and scores it with the unmodified upstream scorer.
  • v1 Parse Document — segmented response with bounding boxes, OCR data, and per-segment processing knobs.
  • v2 Parse Document (Presigned Upload) — segmented response variant with presigned upload for larger files and higher throughput.