Extract structured, source-checked JSON from documents and web pages using your schema.
1. Generate an API key from your dashboard. 2. Send a URL and a JSON Schema describing the data you want. 3. Get structured JSON back — no scraping code required.
curl -X POST https://api.jsonit.io/v1/extract \
-H "Authorization: Bearer sk_live_your_key" \
-H "Content-Type: application/json" \
-d '{
"url": "https://github.com/trending",
"schema": {
"type": "object",
"properties": {
"repos": {
"type": "array",
"items": {
"type": "object",
"properties": {
"name": { "type": "string" },
"description": { "type": "string" },
"stars": { "type": "number" },
"language": { "type": "string" }
}
}
}
}
}
}'{
"success": true,
"url": "https://github.com/trending",
"data": {
"repos": [
{
"name": "anthropics/claude-code",
"description": "Claude Code CLI",
"stars": 12400,
"language": "TypeScript"
}
]
},
"extracted_at": "2026-07-16T00:00:00.000Z"
}Pass your API key as a Bearer token in the Authorization header. Keys are only shown once at creation — if you lose one, revoke it and generate a new one from the dashboard.
Authorization: Bearer sk_live_your_api_key
/v1/extractExtract structured data from a URL using a JSON Schema definition.
{
"url": "string (required) — the page to scrape",
"schema": "object — JSON Schema for the data shape you want (or use extractor_id)",
"extractor_id": "uuid (optional) — a saved extractor; its schema and settings are used",
"accuracy": ""standard" (default) or "high"",
"evidence": "true or false — add a checked page and quote to every item",
"async": "true to run in the background (returns 202 with run_id)",
"batch_id": "uuid you choose, to group runs (optional)",
"webhook_url": "https URL called when a background run finishes (optional)"
}For PDFs, use POST /v1/extract-file with the same fields as multipart/form-data, plus a file (PDF up to 10 MB) or text (text you extracted yourself, up to 2,000,000 characters; keep [Page N] markers in it for page numbers). Every response includes a run_id, and the run is saved to your history.
Accuracy. standard uses a fast, low-cost model and suits clean, simple documents. high uses a frontier model (Claude Sonnet, with backups) that reads dense tables, long documents and engineering text far more reliably, at roughly ten times the cost per page.
Sources. With evidence: true, every item in a list gets a _source with the page and the exact words it came from. Jsonit then checks each quote against the document and each number against its quote:
"_source": {
"page": 22, // where the quote was found
"quote": "SymPoint reaches 83.3 PQ with point features",
"verified": true, // false: quote not found in the document
"unsupported": ["pq"], // numbers that appear nowhere in the document
"not_near_quote": [] // numbers in the document, but not near the quote
}meta.quality sums this up for the whole result (quotes found, numbers to check) and names any lists whose details are still mostly empty, so you know what to review before you rely on it. Sources add about half again to a request's cost.
An extractor is a saved schema plus how to run it: accuracy, sources, and your own rules for that kind of document ("give pressures in psi"). Save one from the Terminal or with the API, then run it by id:
GET /v1/extractors list
POST /v1/extractors { name, schema, description?, instructions?, accuracy?, evidence? }
GET /v1/extractors/:id
PUT /v1/extractors/:id any of the fields above; "corrections": [] clears learned corrections
DELETE /v1/extractors/:id
POST /v1/extract { "url": "...", "extractor_id": "..." }Every extraction is saved as a run, which you can reopen and download from the History page or the API. To correct a result, send the fixed version back. If the run used an extractor, the differences become corrections that the extractor's later runs are told about, so the same mistakes stop recurring.
GET /v1/runs?extractor_id=&batch_id=&limit= newest first, without results
GET /v1/runs/:id status, data, corrected_data, meta
PATCH /v1/runs/:id { "data": { ...corrected result... } }To process many documents, send each with async: true and the same batch_id. Each request returns 202 with a run_id right away and runs in the background. When it finishes, jsonit POSTs to your webhook_url:
{ "event": "run.finished", "run_id": "...", "status": "done", "extractor_id": "...", "batch_id": "...", "source": "spec-23-05-00.pdf" }The webhook carries ids only; fetch the result with GET /v1/runs/:id. Without a webhook, poll GET /v1/runs?batch_id=.... In the Terminal, pick several PDFs at once to run a batch and download every result as one CSV or Excel file.
Zapier and Make. Use their HTTP and Webhooks steps: one step POSTs a file or URL to jsonit with async: true and a webhook_url pointing at a "Catch Hook" trigger in a second workflow, which fetches the run and sends the rows on to Sheets, Airtable or your database.
PDFs that are images of text (scans, stamped submittals, drawings) have no text to read. The Terminal reads those pages with AI vision automatically, and can read every page that way when tables, title blocks or drawings lose their layout as plain text (Page reading: "AI for every page"). From the API, render pages to images and send up to 4 at a time:
POST /v1/ocr { "pages": [ { "page": 1, "image": "data:image/jpeg;base64,..." } ] }
-> { "text": "[Page 1]
...", "billing": {...} }Send the returned text to /v1/extract-file as text. Tables come back as markdown tables and pages keep their [Page N] markers, so sources still show the right page. Reading costs about a cent per page.
Requests are paid from prepaid credit, which you add on the pricing page. Each request is billed at its measured cost (AI tokens processed and pages fetched), with a minimum of $0.001. There's no monthly fee, and credit doesn't expire. Every successful response includes a billing field:
"billing": {
"cost_usd": 0.0041, // what this request cost
"balance_usd": 9.9959 // credit left afterwards
}Before a request runs, its cost is estimated from the document's length (and, for schema suggestions, the council's models). If your balance can't cover the estimate, the request isn't run and returns 402 with balance_usd and estimated_cost_usd. If a request fails after the AI models have already done work, only the raw model cost is charged. Check your balance any time with GET /v1/balance.
To protect the service, each account is limited to 300 requests per hour. Rate limit headers are returned on every response: X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset. When you hit the limit, back off until X-RateLimit-Reset (a unix timestamp) rather than retrying immediately.
POST /v1/suggest-schema (with a url) and POST /v1/suggest-schema-file (with a PDF file or text) have an LLM Council propose a schema. Save a council per subject (invoices, contracts, listings) on the Councils page, then pass its id as council_id. Without one, your default council is used; "builtin" selects the built-in council.
{
"url": "https://example.com/invoice/123",
"council_id": "uuid from the Councils page (optional)"
}New councils can start from a template (research, engineering and construction, financial, legal, listings), a copy, a council file, or ✨ Design with AI: the council architect drafts the lenses, personas, models and domain context from a description of your documents, a sample document, or an existing council to adapt. The same is available as POST /v1/councils/design, which returns an unsaved draft and is billed from your credit:
{
"mode": "describe" | "document" | "adapt",
"prompt": "Electrical specs and utility service standards",
"text": "sample document text (mode document; or \"url\")",
"source_id": "council to adapt (mode adapt)"
}A council can be saved as a JSON file, edited in any text editor, and uploaded again. The layout follows the llm-council-web council format: a description, domain context, shared instructions, a lenses array, and a chairman.
invoices.council.json.$schema line: editors such as VS Code then check the file and suggest field names and models as you type.The format is published as a JSON Schema at jsonit.io/council.schema.json, which also lists every model you can use. Councils from llm-council and llm-council-web (lenses.json, *_Council.json, or an export from the web app) can be imported directly. Their name, description and context are kept, the first two fallback_models become backup models, and lenses marked default_on are kept first when there are more than 6. Because those are review councils, their review instructions are replaced with Jsonit's schema instructions and the built-in chairman is added; ✨ Design with AI → Adapt then turns the reviewers into schema designers. Version 1 files with a single fallback_model still work.
{
"$schema": "https://jsonit.io/council.schema.json",
"format": "jsonit-council",
"version": 2,
"name": "Invoices",
"description": "Vendor invoices and purchase orders.",
"context": "Line items are one object each; money is amount plus ISO 4217 currency; keep invoice and PO numbers exactly as written.",
"instructions": "Propose a JSON Schema ... Return ONLY a JSON object with \"schema\" and \"notes\".",
"chairman": {
"model": "anthropic/claude-sonnet-5.5",
"fallback_models": ["openai/gpt-6.1-sol"],
"instructions": "Merge the proposals into ONE final JSON Schema ... Return ONLY the schema."
},
"lenses": [
{
"id": "ap_clerk",
"name": "Accounts Payable Clerk",
"enabled": true,
"model": "google/gemini-3.8-flash",
"fallback_models": ["openai/gpt-5.4-mini", "cohere/command-a-plus"],
"use_when": "Any invoice or PO.",
"persona": "You process vendor invoices every day. Capture invoice and PO numbers, vendor and bill-to details, every line item, taxes, totals, payment terms and due dates."
}
]
}| Field | What it is | |
|---|---|---|
| name | Council name, up to 60 characters, unique among your councils. If missing, the file name is used. | Optional |
| description | Which documents the council is for, shown when you pick a council. Up to 500 characters. | Optional |
| context | Domain background given to every lens and the chairman: what the documents contain, how the data is used, naming conventions and units. Up to 3,000 characters. | Optional |
| instructions | Task instructions sent to every lens. They must ask for a JSON object with a “schema” key (and optionally “notes”). Up to 6,000 characters. If missing, the built-in instructions are used. | Optional |
| chairman.model | Model that merges the proposals. | Required in chairman |
| chairman.fallback_models | Up to 2 backup models, tried in order if the chairman’s model fails. | Optional |
| chairman.instructions | How to merge the proposals. Must ask for ONLY the final JSON Schema. | Required in chairman |
| lenses[] | 1 to 6 council members. At least one must be enabled. | Required |
| lenses[].name | Shown in the Terminal and in results. Up to 60 characters. | Required |
| lenses[].persona | Who the lens is and what it focuses on (its system prompt). Up to 3,000 characters. | Required |
| lenses[].model | Model this lens runs on. | Required |
| lenses[].fallback_models | Up to 2 backup models, tried in order if the main one fails. | Optional |
| lenses[].use_when | What this lens is good for. Up to 200 characters. | Optional |
| lenses[].enabled | Whether the lens takes part. Defaults to true. | Optional |
| lenses[].id | Short identifier (a–z, 0–9, _ or -). Generated from the name if missing. | Optional |
Other fields (such as _comment, or use_when and cost_tier from lenses.json) are ignored. More lenses and larger models make each suggestion cost more.
| Code | Meaning | What to do |
|---|---|---|
| 400 | Bad request | Missing/invalid `url` or `schema`. Check the request body shape. |
| 401 | Invalid or missing API key | Check the Authorization header; regenerate the key if needed. |
| 402 | Not enough credit | Your balance can’t cover this request’s estimated cost. Add credit, then retry. |
| 404 | Council not found | The `council_id` isn’t one of your councils. Omit it to use your default. |
| 429 | Rate limit exceeded | Wait until X-RateLimit-Reset. |
| 500 | Request failed | Usually a dead page or content the model couldn’t parse. Only raw model costs already incurred are charged; retry or adjust the schema. |
Keep schemas focused — asking for 3-5 well-named fields extracts more reliably than a huge nested schema. Use description fields inside your schema properties to disambiguate (e.g. {"type": "number", "description": "price in USD, no currency symbol"}). See /use-cases for ready-made schemas for common sites and data types.