Extracting structured data

Pull typed fields out of documents using a JSON Schema you provide.

Extract structured data from any document into JSON, guided by your schema.

Extract takes a document and a JSON Schema, and returns the fields described by the schema as a structured object. You define what you want in the schema, the API finds it in the document: invoices, forms, ID documents, contracts, anything.

By default the call is synchronous: you send the request, the API blocks until the extraction is done, and the response comes back with the full result. For larger documents, opt into async mode with options.async = true: you get back a job ID, and you poll until done.

Full request/response schema for POST /api/v1/extract and GET /api/v1/extract/{job_id} lives in the API reference.

When to use Extract

InputOutputBest for
SearchQuery stringRanked text chunksFinding passages across many documents
ParseDocument fileMarkdown textConverting a document to clean text
ExtractDocument + JSON SchemaStructured objectPulling typed fields from forms, invoices, contracts

Use Extract when you need machine-readable values out of a document, mapped to fields you've named.

Extract applies your schema in one of two scopes via options.scope:

  • page (default): independently on every page. result.data has one object per page. Ideal for stacks of invoices, forms, or other self-contained page records.
  • document: once across the full file (pages joined with markers). result.data is a single-element array with the consolidated object. Use this when fields and arrays span pages (auction notices, contracts, multi-page reports).
{ "scope": "document" }

For very large corpora where you only need selected passages, you can still use POST /api/v1/search inside an agentic loop instead of whole-document extract.

Sync extraction (small documents)

Sync mode handles documents up to 20 MB, 15 pages and returns the full result in one response.

const headers = {
  Authorization: `Bearer ${process.env.C212_API_KEY}`,
};

const response = await fetch("https://api.context212.com/api/v1/extract", {
  method: "POST",
  headers: { ...headers, "Content-Type": "application/json" },
  body: JSON.stringify({
    document: "https://example.com/invoices/inv-2025-004.pdf",
    schema: {
      type: "object",
      properties: {
        invoice_number: { type: "string", description: "The invoice reference number" },
        total: { type: "number", description: "The total amount due" },
        due_date: { type: "string", description: "Due date in ISO format" },
      },
    },
  }),
});

const result = await response.json();
console.log(result.status); // → completed
console.log(result.result.data); // → [{ invoice_number: "INV-2025-004", total: 4750.0, due_date: "2025-12-01" }, ...]

You can also send a file via multipart/form-data instead of a URL:

import { readFileSync } from "node:fs";

const form = new FormData();
form.append("file", new Blob([readFileSync("invoice.pdf")]), "invoice.pdf");
form.append(
  "schema",
  JSON.stringify({ type: "object", properties: { invoice_number: { type: "string" } } }),
);

await fetch("https://api.context212.com/api/v1/extract", {
  method: "POST",
  headers,
  body: form,
});

In multipart requests, schema arrives as a JSON-encoded string and is decoded server-side.

Embedded images

Images are on by default for Extract (options.include_images defaults to true when omitted). Embedded PDF images are uploaded, injected into the parsed markdown as ![image N](url) before the LLM runs (so schema fields can capture those URLs), and returned on result.images.

Pass options.include_images: false to skip.

const result = await response.json();
console.log(result.result.images);
// → [{ index: 0, page: 2, url: "https://...", key: "extract/ext_…/0.jpg", mimetype: "image/jpeg", width: 800, height: 600 }, ...]
console.log(result.usage.images_uploaded); // → 3

Multipart: omit the flag (on by default) or -F 'options={"include_images":false}'.

Non-PDF uploads return result.images: [] with usage.images_uploaded: 0 when images are enabled.

Async extraction (large documents)

For documents up to 100 MB, 1000 pages, set options.async = true. The API returns a 202 Accepted immediately with a job ID.

const response = await fetch("https://api.context212.com/api/v1/extract", {
  method: "POST",
  headers: { ...headers, "Content-Type": "application/json" },
  body: JSON.stringify({
    document: "https://example.com/large-report.pdf",
    schema: { type: "object", properties: { title: { type: "string" } } },
    options: { async: true },
  }),
});

const jobId = (await response.json()).id;
console.log(jobId);
// → ext_0196e4b2a3c14d5e8f7a9b2c1d0e3f4a

Poll GET /api/v1/extract/{job_id} until status is completed or failed. Recommended cadence: 1 s for the first 10 s, then 5 s, capped at 30 s.

let data;
while (true) {
  const r = await fetch(`https://api.context212.com/api/v1/extract/${jobId}`, {
    headers,
  });
  data = await r.json();
  if (data.status === "completed" || data.status === "failed") break;
  await new Promise((r) => setTimeout(r, 2000));
}

console.log(data.result.data);

Reading the response

Sync and async responses share the same shape:

{
  "id": "ext_0196e4b2a3c14d5e8f7a9b2c1d0e3f4a",
  "status": "completed",
  "created_at": "2026-03-31T10:00:00+00:00",
  "completed_at": "2026-03-31T10:00:04+00:00",
  "processing_time_ms": 3200,
  "document": {
    "filename": "invoice.pdf",
    "page_count": 3,
    "file_size_bytes": 245120,
    "mime_type": "application/pdf"
  },
  "result": {
    "data": [
      {"invoice_number": "INV-2026-001", "total": null},
      {"invoice_number": null,           "total": 1250.00}
    ],
    "images": [
      {
        "index": 0,
        "page": 1,
        "url": "https://storage.example.com/extract/ext_…/0.jpg?X-Amz-Signature=…",
        "key": "extract/ext_0196e4b2a3c14d5e8f7a9b2c1d0e3f4a/0.jpg",
        "mimetype": "image/jpeg",
        "width": 800,
        "height": 600
      }
    ],
    "pagination": {
      "page": 1,
      "page_size": 15,
      "total_items": 3,
      "total_pages": 1,
      "has_next": false,
      "has_prev": false
    }
  },
  "usage": {
    "pages_processed": 3,
    "images_uploaded": 1
  }
}

With options.scope=page (default), result.data is one entry per page, each shaped like your schema. Fields that weren't found on a given page are null. With options.scope=document, result.data has a single consolidated object. When the document has more than 15 pages, result.data is paginated: request additional pages with ?page=N on the GET /api/v1/extract/{job_id} endpoint.

result.images is present when image upload is enabled (the Extract default). Each entry has a time-limited url (presigned GET) and a durable key in object storage.

Common errors

StatusCause
400Missing document/file, unsupported format, or page limit exceeded
401Missing or invalid API key
404Extract job not found
413File exceeds the size limit (20 MB sync, 100 MB async)
422JSON Schema is malformed, uses unsupported features, or exceeds limits
429Rate limit exceeded (6 requests/second per tenant)
503Parsing backend is overloaded. Retry later

On this page