API documentation

Quickstart

1. Get a free key (200 pages/month, no card):

2. Extract tables from any PDF:

curl -X POST https://<host>/v1/extract \
  -H "X-API-Key: your_key" \
  -F "file=@report.pdf" \
  -F "format=json,markdown"

3. Read the response — the parts that matter:

{
  "tables": [{
    "id": "t1",
    "page_range": [3, 4],        // table spanned two pages: stitched back together
    "confidence": 0.94,          // calibrated: ~94% of tables scored here are fully correct
    "headers": ["Region", "Q1 2026", "Q2 2026"],
    "rows": [["EMEA", "1,204", "1,377"]],   // merged cells resolved, values duplicated
    "spans": [...],              // original merged-cell geometry
    "markdown": "<!-- pages 3-4 -->\n| Region | ... |",  // drops straight into a RAG pipeline
    "notes": ["stitched_across_pages"]
  }],
  "warnings": [],                // {code, message, page?} — switch on code
  "usage": {"pages_billed": 10, "vision_pages": 1, "vision_page_multiplier": 3}
}

Pricing

Free
200 pages/mo
$0
Starter
2,000 pages/mo
$15/mo
Pro
15,000 pages/mo
$49/mo

Scanned pages bill at 3× . Pages that need the vision fallback (scans, image-only pages) count as 3 billed pages each — this is the headline rule, not fine print. Every response's usage block shows exactly what was billed and why. Native digital PDFs bill 1 page per page.

What to expect (honest capability notes)

Interactive reference (auto-generated): /docs (Swagger UI).

Full endpoint reference

Rendered from the canonical contract (docs/API_SPEC.md) — single source of truth, includes the complete error-code and warning-code tables.

API Specification

Status: stable draft — this contract is what Week 2 builds against. Changes require a DECISIONS.md entry.

Base URL: https://<host>/v1 — all routes are versioned under /v1 (decision 2026-07-12). /v2 is the future escape hatch; nothing lives at the root.

Authentication

Every endpoint except GET /v1/health requires an API key.

Channel Mechanism
Direct (Stripe-issued keys) X-API-Key: <key> request header
RapidAPI listing RapidAPI proxy headers (X-RapidAPI-Proxy-Secret), validated by the RapidAPI auth provider

The auth middleware resolves either channel to an internal principal (key id, plan, quota state). Handlers never see platform-specific headers.

Resolution order (production): a request carrying X-RapidAPI-Proxy-Secret is RapidAPI traffic — an invalid secret is rejected outright (origin-direct requests forging RapidAPI headers never fall through to key auth). Otherwise X-API-Key resolves against our issued keys. Otherwise 401. The web tool's /web/extract path is separate (anonymous, rate-limited). The response envelope is identical on every channel. Quotas: direct keys are metered here (vision pages at 3x); RapidAPI plan limits are enforced by RapidAPI's gateway — we record consumption for telemetry only.

Missing/invalid credentials → 401. Exhausted monthly page quota → 429 with code quota_exceeded. Request-rate throttling → 429 with code rate_limited and a Retry-After header.


GET /v1/health

Unauthenticated liveness probe.

Response 200:

{ "status": "ok", "version": "0.1.0" }

POST /v1/extract

Extract tables from a single PDF.

Request

Two accepted content types:

1. multipart/form-data (preferred)

Field Type Required Description
file file yes The PDF.
pages string no Page selection (see below).
format string no Output format selection (see below).

2. application/json (for callers that can't do multipart)

{
  "file_base64": "<base64-encoded PDF bytes>",
  "filename": "report.pdf",
  "pages": "1,3,5-10",
  "format": "json,markdown"
}

pages and format may also be given as query parameters with either content type; query parameters take precedence over body fields.

Parameters

pages — which pages to process. Default "all". 1-indexed. Comma-separated page numbers and inclusive ranges: "1,3,5-10". Out-of-range pages → 400 invalid_pages. Pages are billed only for pages actually processed.

format — which representations each table carries. Default "json". Comma-separated set of:

Value Effect on each table object
json headers + rows + spans populated
markdown markdown populated (GitHub-flavored table)
csv csv populated (RFC 4180, \r\n line endings)

Structural metadata (page_range, confidence, extraction_layer, notes) is always present regardless of format. Unknown format value → 400 invalid_format.

Limits

Limit Value Violation
Max file size 50 MB 413 file_too_large
Max pages per request 200 413 too_many_pages
Max vision-fallback pages per request 30 413 vision_page_limit
Content type PDF only (%PDF magic bytes) 415 unsupported_file_type
Encrypted/corrupt PDF must be openable 422 unprocessable_pdf

The vision-page cap bounds worst-case latency and cost for the synchronous API. Requests that would exceed it fail fast with guidance to split the document or narrow pages; nothing is billed. (Async processing for large scanned documents: see POST /v1/jobs, planned.)

Response 200

{
  "document": {
    "filename": "report.pdf",
    "page_count": 42,
    "pages_processed": 8
  },
  "tables": [
    {
      "id": "t1",
      "page_range": [3, 4],
      "extraction_layer": "fast_path",
      "confidence": 0.94,
      "headers": ["Region", "Q1 2026", "Q2 2026"],
      "rows": [
        ["EMEA", "1,204", "1,377"],
        ["APAC", "980", "1,041"]
      ],
      "spans": [
        { "row": 0, "col": 1, "rowspan": 1, "colspan": 2 }
      ],
      "markdown": "<!-- pages 3-4 -->\n| Region | Q1 2026 | Q2 2026 |\n| --- | --- | --- |\n| EMEA | 1,204 | 1,377 |\n| APAC | 980 | 1,041 |",
      "csv": "Region,Q1 2026,Q2 2026\r\nEMEA,\"1,204\",\"1,377\"\r\nAPAC,980,\"1,041\"\r\n",
      "notes": ["stitched_across_pages", "merged_cells_resolved"]
    }
  ],
  "warnings": [],
  "usage": {
    "pages_billed": 10,
    "vision_pages": 1,
    "vision_page_multiplier": 3
  },
  "processing_time_ms": 843,
  "request_id": "req_8f2c1a"
}

Field reference

document

Field Type Description
filename string | null As uploaded / provided; null if unknown.
page_count int Total pages in the PDF.
pages_processed int Pages actually examined after pages selection.

tables[] — ordered by first page, then vertical position.

Field Type Description
id string Stable within the response (t1, t2, …).
page_range [int, int] First and last 1-indexed page the table spans. Single-page tables repeat the page: [3, 3].
extraction_layer "fast_path" | "vision_fallback" Which layer produced the cells. Stitched tables mixing layers report "vision_fallback" (the costlier/weaker signal wins).
confidence float 0.0–1.0, per-table. Calibrated: ~0.9 should mean ~90% of such tables are fully correct. Honest scores are a product feature — never inflate.
headers string[] | null Detected header row(s), flattened to one label per column. null if no header detected (rows then start at the first data row — nothing is guessed).
rows (string | null)[][] Data rows, header rows excluded. Merged cells are resolved: the value is duplicated into every covered position. Empty cells are null, not "".
spans object[] Original merged-cell geometry: {row, col, rowspan, colspan}, 0-indexed against rows (header spans indexed against headers are not reported). Empty array if no merges.
markdown string Only when format includes markdown. Starts with a <!-- pages N-M --> provenance comment (RAG pipelines want source pages). If no header was detected, a synthesized col_1, col_2, … header row is used (GFM requires one) and the table's confidence is capped.
csv string Only when format includes csv. Header row present only when actually detected — never synthesized.
notes string[] Machine-readable processing flags. Known values: stitched_across_pages, merged_cells_resolved, header_inferred, ragged_rows_padded, low_confidence_cells.

warnings[] — document/page-level conditions, structured (decision 2026-07-14):

{ "code": "vision_unavailable", "message": "page 7: vision fallback not configured; fast-path results returned (may be low quality)", "page": 7 }
Field Type Description
code string Stable snake_case identifier — switch on this, never parse message. Vocabulary below; new codes may be added (treat unknown codes as informational).
message string Human-readable; wording may change freely between releases.
page int Optional, 1-indexed; present when the warning is page-scoped.

Scoping rule: warnings are document/page level; table-scoped conditions are machine-readable flags in each table's notes. When a table-level condition is worth surfacing at document level (e.g. ambiguous_merge), the warning carries the page and the table keeps its note — clients rendering per-table UI should read notes; clients triaging whole documents should read warnings. The two mechanisms never replace each other.

Warning code vocabulary:

code page? Emitted when
vision_unavailable yes The page needed the vision fallback but no provider is configured; fast-path results returned instead.
vision_parse_failed yes The vision provider responded but never produced parseable table JSON (after one correction retry); fast-path results returned instead.
vision_page_limit_partial yes Reserved — not currently emitted. Requests over the vision-page cap fail with 413 vision_page_limit today; this code is pre-registered so a future partial-processing mode is non-breaking.
ambiguous_merge yes Empty cells look like merged cells but the pattern is ambiguous; left empty rather than guessed, table confidence lowered (table note: ambiguous_merge_left_empty).
stitch_uncertain yes A multi-page table was stitched on column alignment alone (no repeated header on the continuation page); verify the join (table note: stitched_across_pages).

usage

Field Type Description
pages_billed int Pages counted against the monthly quota. Vision pages bill at 3x: pages_billed = (pages_processed − vision_pages) + 3 × vision_pages.
vision_pages int Pages that required the vision fallback.
vision_page_multiplier int Currently always 3. Echoed so billing is auditable per response. Must appear on any pricing page — it is not fine print.

A PDF with no detectable tables is a success: 200 with tables: [] and an explanatory warning.

Response headers

Header Description
X-Request-Id Mirrors request_id; include in support requests.
Retry-After On 429 only. Seconds to wait.

Errors

All errors use one envelope:

{
  "error": {
    "code": "file_too_large",
    "message": "PDF is 61.2 MB; the limit is 50 MB.",
    "request_id": "req_8f2c1a"
  }
}

message is neutral, human-readable prose — no marketing copy, and wording may change; switch on code. An optional docs_url field may appear when there is a relevant pointer (e.g. rate_limited includes where to get an API key); it is omitted otherwise.

HTTP code When
400 invalid_pages Unparseable or out-of-range pages value.
400 invalid_format Unknown format value.
400 invalid_request Missing file, malformed base64, malformed multipart.
401 unauthorized Missing or invalid API key.
413 file_too_large File exceeds 50 MB.
413 too_many_pages Page selection exceeds 200 pages.
413 vision_page_limit Document needs more than 30 vision-fallback pages in one request. Message includes guidance: split the document or narrow pages. Nothing is billed.
415 unsupported_file_type Not a PDF.
422 unprocessable_pdf Encrypted, corrupt, or zero-page PDF.
429 rate_limited Request-rate throttle. Has Retry-After.
429 quota_exceeded Monthly page quota exhausted.
500 internal_error Our bug. Nothing billed.
503 vision_unavailable Vision provider outage and the document required fallback. Fast-path-only documents are unaffected by provider outages.

Billing rule: pages are billed only on 2xx responses, per the formula in usage above.


POST /v1/jobs — planned, not implemented

Reserved for async processing of documents that exceed the synchronous vision-page cap (decision 2026-07-12). Do not build against this section; it exists so adding async later is non-breaking. Intended shape:

Open design questions (deliberately unresolved): result retention window vs. the process-and-discard invariant, webhook completion callbacks, job-level page limits.


Plans (for reference)

Plan Pages / month Price
Free 200 $0
Starter 2,000 $15
Pro 15,000 $49