pdfmux/blog
pdf-extraction

PDF extraction with OpenAI: structured outputs, vision, and the limits

TL;DRHow to extract structured data from PDFs with OpenAI's GPT-4o and response_format json_schema, what it costs, and where a dedicated extractor still wins.

Direct answer: OpenAI’s Chat Completions and Responses APIs can take a PDF as input and return extracted fields as JSON if you pass response_format with a json_schema and strict: true. That gets you correctly-shaped JSON on the first try, for occasional or low-volume extraction, with zero setup beyond an API key. What it does not get you: per-page confidence, detection of silently dropped pages, or an offline/local run, and cost scales per call with no ceiling. For a few dozen PDFs a week, it is the fastest path. For a production pipeline processing thousands of documents, the gaps below start to matter, and a dedicated extractor with page-level confidence — like pdfmux — closes them.


The actual API call

OpenAI’s Chat Completions API accepts PDF files as base64-encoded input alongside a JSON schema in response_format. Here’s a minimal working extraction — pulling invoice fields from a PDF:

import base64
from openai import OpenAI

client = OpenAI()

with open("invoice.pdf", "rb") as f:
    pdf_b64 = base64.b64encode(f.read()).decode("utf-8")

schema = {
    "type": "object",
    "properties": {
        "invoice_number": {"type": "string"},
        "total_due": {"type": "number"},
        "due_date": {"type": "string"},
        "line_items": {
            "type": "array",
            "items": {
                "type": "object",
                "properties": {
                    "description": {"type": "string"},
                    "amount": {"type": "number"},
                },
                "required": ["description", "amount"],
                "additionalProperties": False,
            },
        },
    },
    "required": ["invoice_number", "total_due", "due_date", "line_items"],
    "additionalProperties": False,
}

response = client.chat.completions.create(
    model="gpt-4o",
    messages=[
        {
            "role": "user",
            "content": [
                {"type": "text", "text": "Extract the invoice fields."},
                {
                    "type": "file",
                    "file": {
                        "filename": "invoice.pdf",
                        "file_data": f"data:application/pdf;base64,{pdf_b64}",
                    },
                },
            ],
        }
    ],
    response_format={
        "type": "json_schema",
        "json_schema": {"name": "invoice", "strict": True, "schema": schema},
    },
)

print(response.choices[0].message.content)

strict: true is the part that matters. Without it, the model is free to add extra keys, skip required fields, or return a string where you asked for a number — you’d need your own validation and retry loop on top. With it, the API constrains token generation so the output is guaranteed to parse against your schema. That guarantee is about shape, not truth: a strict-mode response can still put a hallucinated total in a validly-typed number field.

How GPT-4o actually reads the PDF

There’s no separate “PDF understanding” model. GPT-4o handles a PDF by extracting whatever text layer exists and rendering each page as an image, then reasoning over both, the same vision pathway used for a photo. That’s why it can read a scanned contract with no text layer at all: it’s doing OCR-by-vision on the rendered page, not parsing PDF structure. It’s also why results vary with page complexity the way vision-model OCR generally does — dense multi-column tables and rotated scans are harder for it than a clean single-column invoice, and there’s no page-level signal telling you which case you’re in.

What you don’t get

Three gaps show up once you move past a demo:

No per-page confidence. The API returns your JSON and nothing else. If page 4 of a 12-page contract was too blurry to read and the model quietly filled in a plausible-looking clause instead of flagging it, you have no signal that happened. You find out when someone downstream acts on the wrong clause.

No silent-drop detection. If the model skips a page’s content and returns valid JSON anyway (because the schema didn’t require anything from that specific page), the response still validates. There’s no report of “page 4 was not usable” the way a dedicated per-page audit produces. We’ve written before about why this exact failure mode is the one that breaks RAG pipelines weeks after deployment, and it applies to any extractor, OpenAI’s API included.

No offline option. Every PDF goes through OpenAI’s servers. For a pipeline that touches account statements, medical records, or contracts under an NDA, that’s not a config flag you can turn off — it’s a property of using the hosted API at all.

What it costs at volume

As of this writing, standard GPT-4o pricing on OpenAI’s pricing page is $2.50 per 1M input tokens and $10.00 per 1M output tokens; gpt-4o-mini is $0.15 / $0.60 per 1M tokens for lighter-weight extraction where accuracy requirements allow it. A single page, rendered as an image plus whatever extracted text layer exists, plus a moderate-size JSON response, typically runs a few thousand tokens round-trip, so cost scales close to linearly with page count. There’s no free tier for volume beyond OpenAI’s general new-account credits, and no ceiling: a spike in document volume is a proportional spike in the bill, with no local fallback if the API is rate-limited or down.

When the API approach is the right call

None of this makes structured outputs the wrong tool. If you’re extracting a few dozen PDFs a week, need zero infrastructure, and the documents are clean enough that occasional silent errors are tolerable (or a human reviews every output anyway), response_format: json_schema with strict: true is genuinely the fastest path from PDF to JSON. It’s also a reasonable first pass for documents with genuinely irregular layouts, where a vision model’s general reasoning beats a rules-based layout parser.

Multi-page documents and the context window

A single chat.completions.create call can take a whole multi-page PDF, but every page you add is more image tokens plus more extracted text competing for the same context window, and a long document pushes the model to skim rather than read closely — the mechanism behind the silent-drop problem above, not a separate bug. For anything past a few dozen pages, split by section or page range and make one call per chunk with the same schema, then merge the results. This also caps the damage from a single bad page: a hallucinated field in chunk 3 doesn’t put chunk 3’s error into every other chunk’s context.

Two practical defaults worth setting from the start:

  • Keep additionalProperties: false on every object in the schema. Without it, strict mode still allows the model to attach fields you didn’t ask for, and downstream code that assumes a fixed key set breaks quietly.
  • Log the raw model response alongside the parsed JSON, not just the parsed JSON. When a value looks wrong three weeks later, the raw response is the only way to tell whether the model misread the page or your schema description was ambiguous.

Validating output you can’t otherwise check

Strict mode validates shape at generation time, but nothing in the API validates that a number came from the page rather than the model’s prior on what invoices “usually” contain. The cheapest check available without a second model call is a sanity pass on your own data: totals that should sum (line items vs. invoice total), dates that should fall in a plausible range, and required fields that come back suspiciously identical across documents — often a sign the model is pattern-matching a template rather than reading the actual page. None of that catches a plausible-but-wrong single value, which is exactly the class of error verify_extraction is built to catch by comparing extracted values back against the source document rather than trusting the extractor’s own output.

When you need more than the API gives you

Once volume, cost predictability, data residency, or “did this silently fail” become real requirements, the gaps above stop being edge cases. pdfmux runs locally, scores 0.903 on the opendataloader-bench of 200 real-world PDFs (ranking #2 of 8 engines measured), and reports per-page confidence plus a verify_extraction call that flags pages an extractor silently dropped or mangled — the exact signal missing from a raw API response:

pip install pdfmux
pdfmux convert invoice.pdf --format json

The two approaches aren’t mutually exclusive. A common pattern: extract with pdfmux for structure and per-page confidence, then hand only the low-confidence pages to a vision model like GPT-4o for a second opinion — paying API cost only where it’s actually needed, instead of on every page of every document.

FAQ

Can OpenAI extract data directly from a PDF? Yes. The Chat Completions and Responses APIs accept PDF files as input and can return extracted fields as JSON when you set response_format to a json_schema. GPT-4o reads the PDF as a mix of extracted text and rendered page images, so it handles scanned pages the same call handles text-based ones.

Is OpenAI’s structured output guaranteed to match my schema? With strict mode on, yes for the shape: the API constrains decoding so the output always validates against your JSON schema. It does not guarantee the values inside that shape are correct — a strict-mode response can still have a hallucinated number in a perfectly valid field.

What does it cost to extract PDFs with GPT-4o at scale? As of this writing, standard GPT-4o pricing is $2.50 per 1M input tokens and $10.00 per 1M output tokens (see OpenAI’s pricing page for current rates). A page rendered as an image plus a moderate JSON response runs a few thousand tokens combined, so cost scales roughly linearly with page count with no volume discount below the batch API tier.

Frequently asked questions

Can OpenAI extract data directly from a PDF?

Yes. The Chat Completions and Responses APIs accept PDF files as input and can return extracted fields as JSON when you set response_format to a json_schema. GPT-4o reads the PDF as a mix of extracted text and rendered page images, so it handles scanned pages the same call handles text-based ones.

Is OpenAI's structured output guaranteed to match my schema?

With strict mode on, yes for the shape: the API constrains decoding so the output always validates against your JSON schema. It does not guarantee the values inside that shape are correct — a strict-mode response can still have a hallucinated number in a perfectly valid field.

What does it cost to extract PDFs with GPT-4o at scale?

As of this writing, standard GPT-4o pricing is $2.50 per 1M input tokens and $10.00 per 1M output tokens (see OpenAI's pricing page for current rates). A page rendered as an image plus a moderate JSON response runs a few thousand tokens combined, so cost scales roughly linearly with page count with no volume discount below the batch API tier.