SchemaExtractSchemaExtract home

Self-serve API

Document to JSON API

Copy a curl, send a file, get schema-shaped JSON. No sales call. Free playground first. Default cloud path is an OpenRouter-compatible endpoint with free model ids — paid OpenAI or Anthropic keys are optional.

OpenAPI 3.1No card to tryopenapi.jsonllms.txt

POST /v1/extract

Accepts multipart form-data (`file` + `schema`) or a JSON body (`file_base64` + `schema`). Optionally pass `preset` as invoice | packing_list | bank_statement | bol. When AUTH_SECRET is set and DEMO_MODE is not true, send Authorization: Bearer sk_live_…

Multipart
file + schema and/or preset
curl -sS -X POST https://schemaextract.shop/v1/extract \
  -H "Authorization: Bearer sk_live_…" \
  -F "preset=invoice" \
  -F "file=@fixtures/invoice-acme.pdf"
JSON body
schema + file_base64 + filename
curl -sS -X POST https://schemaextract.shop/v1/extract \
  -H "Content-Type: application/json" \
  -d "{\"preset\":\"invoice\",\"filename\":\"invoice-acme.pdf\",\"file_base64\":\"<base64>\"}"

Async jobs (slow models / Cloudflare 524)

trycloudflare.com and similar proxies often close an idle POST around 100 seconds. Local Ollama extracts commonly take 2–5 minutes. Enqueue a job, then poll. Quota is checked on enqueue and counted only if the job succeeds. File bytes stay in RAM and are dropped when the job finishes — they are not written to the account store.

POST /v1/extract/jobs
202 immediately, then GET the poll_url
curl -sS -X POST https://schemaextract.shop/v1/extract/jobs \
  -F "preset=packing_list" \
  -F "file=@fixtures/packing-list-northwind.png"
# On Vercel the POST awaits the job (still pollable via GET).

curl -sS https://schemaextract.shop/v1/extract/jobs/job_…
Same via POST /v1/extract
?async=1, Prefer: respond-async, or Accept: application/vnd.schemaextract.job+json
curl -sS -X POST "https://schemaextract.shop/v1/extract?async=1" \
  -H "Prefer: respond-async" \
  -F "preset=invoice" \
  -F "file=@fixtures/invoice-acme.pdf"

Success body

{
  "ok": true,
  "data": { "vendor": "Acme Industrial Supply Co.", "total": 4820.75 },
  "meta": {
    "model": "mock",
    "pages": 2,
    "preset": "invoice",
    "filename": "invoice-acme.pdf",
    "mode": "mock",
    "warnings": [],
    "plan": "free",
    "quota_remaining": 19
  }
}

Error body

{
  "ok": false,
  "error": { "code": "INVALID_SCHEMA", "message": "Schema root must be a JSON object, not an array." }
}

Limits

  • File: PDF / PNG / JPG / WebP, max 10 MB, first 3 pages.
  • Schema: object root, max 50 top-level keys, depth 8, 200 total keys, 64 KB.
  • Burst rate limit: 60 requests / 10 minutes per client → 429 RATE_LIMITED with X-RateLimit-*.
  • Monthly quotas: Free 20 · Starter 500 · Pro 5000402 QUOTA_EXCEEDED.
  • Auth: when AUTH_SECRET is set and DEMO_MODE is not true, send Authorization: Bearer sk_live_….
  • Files stay in memory for the request (sync) or until the async job finishes. Nothing is written to disk except account metadata in the JSON/KV store.

Presets

Invoice
invoice

vendor, invoice_number, date, currency, total, line_items

Packing list
packing_list

shipper, consignee, marks, packages

Bank statement
bank_statement

account_holder, period_start, period_end, transactions

Bill of lading
bol

bol_number, shipper, consignee, vessel, port_of_loading, port_of_discharge, containers

Other endpoints

  • POST /v1/extract — sync extract (short cloud-LLM runs)
  • POST /v1/extract/jobs · GET /v1/extract/jobs/:id — async extract (Ollama / long runs)
  • GET /health — liveness + feature flags
  • POST /v1/auth/magic-link — passwordless sign-in
  • GET/POST /v1/keys · DELETE /v1/keys/:id
  • POST /v1/billing/checkout · /v1/billing/portal · /v1/billing/webhook

Error codes

  • BAD_REQUEST
  • INVALID_JSON
  • INVALID_SCHEMA
  • SCHEMA_TOO_LARGE
  • INVALID_PRESET
  • MISSING_FILE
  • UNSUPPORTED_FILE
  • FILE_TOO_LARGE
  • MODEL_FAILURE
  • RATE_LIMITED
  • UNAUTHORIZED
  • PAYMENT_REQUIRED
  • QUOTA_EXCEEDED