Matte gouache painting in beige, charcoal, bronze and warm gold: an excavation trench where a rectangular block of stratified earth opens like a drawer, revealing a precise grid of small compartments, each holding one artifact — a coin, a key, a seal — while the surrounding strata stay rough and unsorted
All posts

Turn PDFs and images into JSON with one API call

AIVAX's Extraction API returns one JSON object per document, shaped by the schema you send. How the schema drives the search, what comes back when a field is missing, and when to extract instead of summarizing or fetching.

An invoice PDF has maybe twenty values a downstream system cares about: the vendor, the total, the due date, the line items. A layout parser will happily return all four hundred elements on the page with their coordinates, and then your code gets to find the twenty. Schema-driven extraction reverses that work: you declare the twenty fields once, and the API returns one JSON object per document with exactly those fields.

The Extraction API is AIVAX's endpoint for this. One POST request takes a JSON schema and an array of documents — text, images, PDFs — and returns one object per document, in input order, with each field found, typed, or left null.

The schema is the search plan

The endpoint is POST /api/v1/generations/extract. The body carries a responseSchema, a documents array, and an optional instructions string:

{
  "instructions": "Only consider the issuing company as the vendor.",
  "responseSchema": {
    "type": "object",
    "properties": {
      "invoice_number": { "type": "string", "description": "invoice number" },
      "vendor": { "type": "string", "description": "company that issued the invoice" },
      "total": { "type": "number", "description": "total amount due" },
      "line_items": {
        "type": "array",
        "items": {
          "type": "object",
          "properties": {
            "product": { "type": "string", "description": "product name" },
            "quantity": { "type": "integer", "description": "number of units" }
          }
        }
      }
    }
  },
  "documents": [
    {
      "type": "file",
      "file": {
        "filename": "invoice.pdf",
        "file_data": "https://example.com/invoices/invoice.pdf"
      }
    }
  ]
}

Each property is a field to find, and its description is what the extraction looks for, so write it the way a person would search the document: "total amount due" rather than a bare "total". Nested objects and arrays of objects are supported; $ref and $defs are not, so spell the schema out in full. The instructions string applies to every document in the request and is where scope rules belong, like which of three companies on a page counts as the vendor.

The schema must be an object with at least one property. That constraint is the API's shape in miniature: extraction is for records, not for free-form answers — if you want prose, that is a different endpoint.

One object per document, in input order

The documents array takes OpenAI-style content parts: text, image_url, or file. A single request can mix them — a pasted email body, a photographed receipt, a twelve-page PDF. Images and files accept base64 data URLs or public HTTP(S) URLs in JPEG, PNG, TIFF, or PDF, multipage included. AIVAX downloads public URLs before processing, up to 5 MB each, so they have to be reachable without sign-in; base64 data URLs are limited by the request size instead.

Documents are processed independently, four at a time, and the response's data array holds one object per document in the same order as the request — so document 3 in, document 3 out, whatever the processing order was. The independence has a cost side too: if any document fails, the whole request returns an error instead of partial results, and the documents that finished before the failure are still billed. We walked through the code that causes this in what a failed extraction batch costs, including why sending one document per request keeps the blast radius of a bad scan to one read.

A missing field is null; a type converts only when it parses

The schema declares intent, and the response honors it when it can. A field the model cannot find comes back as null. A field declared as number, integer, or boolean is converted to that type when the value parses, and kept as a string when it does not — so "R$ 1.234,56" arrives as text, not as a number, and the failed-batch post has the list of which separators and currency symbols survive the conversion. The extraction docs state the same caveat from the other side: extracted values are model-generated and can be missing or wrong, and amounts and identifiers deserve a check against the original document. Treat the schema as a request with a strong opinion, and validate the types on your side.

Choose by what the next step consumes

Extraction shares its request shape with two siblings, and the choice between them is about the consumer of the result.

When the next step is a system that needs values — a billing table, a CRM record, a router that keys off a total — extract. When the next step is a person or a rule that needs the gist, summarizing the document returns a condensation instead of fields. And when the next step needs the full text — a RAG index, a reader — you want Fetch for URLs or the PDF-to-Markdown pipeline for documents, because extraction collapses exactly the detail that retrieval exists to surface.

The bill is for reading

Extraction is billed at $0.04 per million input tokens, and output tokens are not billed. The expensive part is reading the document once; the JSON you keep costs nothing extra. Quota is shared with summarization and counts documents, not HTTP requests: the free plan allows 10 documents per minute and 300 per day, Pro allows 100 per minute, Max 1,000 and Ultimate 2,500, per the plans and limits page. A request that would exceed an active window returns 429 before any document is processed.


Behavior described here was read from the Extraction documentation and the service implementation on November 2, 2026. On November 10, 2026 the request in this post was run against the live AIVAX API with a text document and a PDF URL, returning one object per document in input order with null and an empty array for a PDF that held no invoice; a schema with no properties returned 400, and R$ 1.234,56 stayed a string while 1234.56 became a number. The quota windows and the 429 behavior come from the documentation and the service implementation.