Files API, vision & PDF
Claude reads pictures and PDFs, not just text. This cookbook shows the API mechanics: image and document content blocks, inline base64 vs the upload-once Files API, document Q&A with citations, and the cost and cleanup discipline production needs.
Learning objectives
- Send an image to Claude as an inline base64
imagecontent block. - Send a PDF as a
documentblock and ask questions about it. - Upload a file once with the Files API and reference it by
file_idacross many calls. - Decide when to inline bytes vs upload to the Files API.
- Turn on citations so answers point back to the source document.
- Budget image/document token cost and clean up files in production.
1 · Two ways to give Claude a file essential
Claude can read images and PDFs, not just text. There are exactly two ways to hand it a file, and everything in this lesson is a variation on one of them. Inline: you put the raw bytes (base64-encoded) directly inside the message as a content block — one self-contained request. Files API: you upload the file once, get back a file_id, and then reference that id in as many messages as you like — the bytes are stored on Anthropic's side and never re-sent.
The message body is the same shape either way. A user message's content is a list of blocks. For a picture you send an {"type": "image", ...} block; for a PDF or text file you send an {"type": "document", ...} block; and you usually add a {"type": "text", ...} block with your question. The only thing that changes between the two paths is the block's source: raw base64 bytes inline, versus a {"type": "file", "file_id": ...} reference.
The top diagram is the inline path — the whole file rides inside one request.
- Your file is a picture or PDF on your disk.
- Inline base64 — you encode the raw bytes as base64 text and drop them straight into a content block. The bytes become part of the message.
- messages.create — a single API call carries the block (bytes and all) up to Anthropic.
- Claude reads it and answers. Simple, self-contained — but the bytes travel on every call you make.
In short: One file, one question, one request. Everything is in that request; nothing is stored afterward.
The bottom diagram is the Files API path — upload the bytes once, then pass a tiny id forever after.
- files.upload sends the bytes to Anthropic one time and hands back a
file_id— a short string that names the stored file. - file_id is all you keep. It's a reference, not the bytes.
- many messages.create — each later call includes the
file_idinstead of the file. The bytes never travel again. - Claude reads the stored file on every call, just as if you'd inlined it — but you only paid to move the bytes once.
In short: Compare the two pictures: the top re-sends bytes each call; the bottom sends them once. That difference is the whole reason the Files API exists.
Read the two diagrams together: the top one uploads the bytes on every call; the bottom one uploads once and then passes a small id string forever after. Same picture reaching Claude — very different network and cost profile when you ask more than one question.
2 · Recipe: an image as inline base64 essential
The most direct way to send a picture: read the file, base64-encode the bytes, and put them in an image block whose source.type is "base64". You must tell the API the media_type (e.g. image/png, image/jpeg, image/gif, image/webp). This block is REAL SDK code — it opens a real file and calls the API.
image_inline.py# needs: pip install anthropic + ANTHROPIC_API_KEY (+ a real image/PDF file)
import base64
import anthropic
client = anthropic.Anthropic() # reads ANTHROPIC_API_KEY from the environment
with open("chart.png", "rb") as f:
image_b64 = base64.standard_b64encode(f.read()).decode("utf-8")
resp = client.messages.create(
model="claude-opus-4-8",
max_tokens=1024,
messages=[{
"role": "user",
"content": [
{
"type": "image",
"source": {
"type": "base64",
"media_type": "image/png", # match the actual file type
"data": image_b64,
},
},
{"type": "text", "text": "What trend does this chart show?"},
],
}],
)
print(next(b.text for b in resp.content if b.type == "text"))
This is the most direct recipe: read an image file, base64-encode it, and send it as an image block. It hits the real API, so it needs a key and network — the ▶ needs API key + network label flags that.
anthropic.Anthropic()builds the client; it picks upANTHROPIC_API_KEYfrom the environment, so the key never appears in code.base64.standard_b64encode(f.read()).decode("utf-8")turns the raw image bytes into the base64 text the API expects.- The
contentis a list of blocks: animageblock (withsource.type == "base64"and the matchingmedia_type) followed by atextblock holding your question. next(b.text for b in resp.content if b.type == "text")pulls the first text block out of the reply — the reply is a list of blocks too.
Try this: Point open("chart.png") at a real image you have and change the question. The media_type must match the file — use image/jpeg for a .jpg.
"source": {"type": "url", "url": "https://…/chart.png"}. Anthropic fetches the image for you — handy when the file already lives on a public URL and you don't want to download-then-re-upload it yourself.3 · See the block shape — offline essential
Before spending a single token, it helps to see what an image content block actually is: a plain Python dict. The helper below takes raw bytes and a media type, base64-encodes them with the standard library, and builds the exact block the SDK sends. It uses a tiny hardcoded PNG byte literal so it needs no file, no key, and no network — it just prints the block's shape. This is the one recipe on the page you can run right now.
block_shape.pyimport base64
# A tiny hardcoded PNG (the 1x1 transparent pixel) — no file needed.
PNG_BYTES = (
b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR"
b"\x00\x00\x00\x01\x00\x00\x00\x01\x08\x06"
)
def image_block(raw: bytes, media_type: str) -> dict:
"""Build the inline base64 image content block the SDK would send."""
b64 = base64.standard_b64encode(raw).decode("utf-8")
return {
"type": "image",
"source": {"type": "base64", "media_type": media_type, "data": b64},
}
block = image_block(PNG_BYTES, "image/png")
print("type: ", block["type"])
print("source.type: ", block["source"]["type"])
print("media_type: ", block["source"]["media_type"])
print("data length: ", len(block["source"]["data"]), "base64 chars")
print("data preview:", block["source"]["data"][:16], "...")
type: image
source.type: base64
media_type: image/png
data length: 36 base64 chars
data preview: iVBORw0KGgoAAAAN ...
This is the one recipe you can run right now — pure standard library, no key, no network. It shows that an image content block is just a Python dict, using a tiny hardcoded PNG so there's no file to find.
PNG_BYTESis a hardcoded byte literal (the start of a 1x1 PNG). Because it's baked in, the script needs no file on disk.image_block(raw, media_type)base64-encodes the bytes and assembles the exact dict the SDK sends: atype: "image"with asourceholdingtype,media_type, anddata.- The
printlines pull each field back out so you can see the shape — the same shape every image recipe on this page produces.
What the output means: You see the block's fields printed: type: image, source.type: base64, the media type, and the base64 data length plus a short preview. That dict is all the SDK ever sends over the wire.
Try this: Swap in your own tiny bytes literal, or change the media_type, and re-run. The data length changes with the input; the structure never does.
4 · Recipe: a PDF document & document Q&A intermediate
PDFs use a document block instead of an image block, with media_type: "application/pdf". Claude reads both the text and the visual layout of each page — tables, charts, signatures — so you can ask questions that depend on how the page looks, not just its extracted text. Put the document block before your question text. This is REAL SDK code.
pdf_inline.py# needs: pip install anthropic + ANTHROPIC_API_KEY (+ a real image/PDF file)
import base64
import anthropic
client = anthropic.Anthropic()
with open("contract.pdf", "rb") as f:
pdf_b64 = base64.standard_b64encode(f.read()).decode("utf-8")
resp = client.messages.create(
model="claude-opus-4-8",
max_tokens=1024,
messages=[{
"role": "user",
"content": [
{
"type": "document",
"source": {
"type": "base64",
"media_type": "application/pdf",
"data": pdf_b64,
},
},
{"type": "text", "text": "What is the termination clause? Quote it."},
],
}],
)
print(next(b.text for b in resp.content if b.type == "text"))
Same idea as the image recipe, but for a PDF — the block type is document instead of image. This is real API code (▶ needs API key + network).
- The
media_typeis"application/pdf". Claude reads the text and the visual layout of each page, so questions about tables or signatures work. - The
documentblock comes before thetextblock — put the file first, then the question about it. - Otherwise the request is identical to the image recipe: base64 the bytes, build the block list, read the first text block out of the reply.
Try this: This is document Q&A — swap the question for "list every date mentioned" and Claude answers from the same PDF. Remember inline requests cap at 32 MB total, so a huge scanned PDF needs the Files API instead.
| Limit (inline base64) | Value |
|---|---|
| Max request size | 32 MB total |
| Max PDF pages | 600 (100 for 200K-context models) |
| Image formats | PNG, JPEG, GIF, WebP |
| Newlines in base64 | must be stripped (the SDK does this for you) |
5 · Recipe: upload once, reuse with the Files API intermediate
The Files API is the answer to "I have one document and twenty questions". You upload the file once and get back a file_id. Every later message references that id via a {"type": "file", "file_id": ...} source — the bytes never travel again. The Files API is beta, so you call it on the client.beta.* namespace and pass betas=["files-api-2025-04-14"] on both the upload and any messages.create that references the file. This is REAL SDK code.
files_api.py# needs: pip install anthropic + ANTHROPIC_API_KEY (+ a real image/PDF file)
import anthropic
client = anthropic.Anthropic()
# 1. Upload ONCE. The file argument is a (filename, fileobj, media_type) tuple.
uploaded = client.beta.files.upload(
file=("contract.pdf", open("contract.pdf", "rb"), "application/pdf"),
betas=["files-api-2025-04-14"],
)
print("file_id:", uploaded.id, "| size:", uploaded.size_bytes, "bytes")
# 2. Reference the SAME file_id across many calls — bytes never re-sent.
questions = [
"What are the key terms?",
"What is the termination clause?",
"Summarize the payment schedule.",
]
for q in questions:
resp = client.beta.messages.create(
model="claude-opus-4-8",
max_tokens=1024,
messages=[{
"role": "user",
"content": [
{"type": "document", "source": {"type": "file", "file_id": uploaded.id}},
{"type": "text", "text": q},
],
}],
betas=["files-api-2025-04-14"],
)
answer = next(b.text for b in resp.content if b.type == "text")
print(f"\nQ: {q}\nA: {answer[:120]}")
# 3. Manage stored files. Uploads count against a per-org storage quota.
for f in client.beta.files.list(betas=["files-api-2025-04-14"]):
print(f.id, f.filename, f.size_bytes)
# 4. Clean up when you're done with it.
client.beta.files.delete(uploaded.id, betas=["files-api-2025-04-14"])
This recipe is "one document, many questions" done right: upload once, then reference the stored file by id on every later call. Real API code — and the Files API is beta, so note the client.beta.* calls and the betas=[...] argument.
- Step 1 —
client.beta.files.upload(...)sends the bytes once and returns an object whose.idis yourfile_id. - Step 2 — the loop asks three questions. Each message uses a
documentblock whose source is{"type": "file", "file_id": uploaded.id}— the bytes are never re-sent, only the id. - Step 3 —
client.beta.files.list(...)shows what's stored; uploads count against your organization's storage quota. - Step 4 —
client.beta.files.delete(uploaded.id, ...)cleans up. Uploaded files persist until you delete them, so always pair an upload with a cleanup.
Try this: Note the betas=["files-api-2025-04-14"] on both the upload and each messages.create — the beta header is required on any call that touches the file.
image block: {"type": "image", "source": {"type": "file", "file_id": id}}. The block type must match the file's MIME type — document for PDFs/text, image for pictures.6 · Inline vs Files API — choosing advanced
Both paths get the same bytes to Claude. The decision is about how many times you'll ask and how big the file is:
| Question | Inline base64 | Files API |
|---|---|---|
| How many times you'll ask | once | many times |
| Bytes re-sent per call | every call | never (only the id) |
| File size ceiling | 32 MB per request | 500 MB per file |
| State to manage | none | stored files + a quota to clean up |
| Beta header needed | no | yes (files-api-2025-04-14) |
Rule of thumb: inline for a one-shot (extract a field from this one receipt), Files API when the same document drives many turns (a contract you'll interrogate across a conversation, or a reference doc shared by a batch of requests). The Files API also lets a big file exceed the 32 MB inline request cap, since the bytes aren't part of the message.
cache_control breakpoint after it so its processed tokens are cached too. The Files API saves re-uploading the bytes; caching (see AP2) saves re-processing them into tokens. Together they cut both bandwidth and cost.7 · Recipe: citations that point at the source advanced
For document Q&A you often want the answer to cite where it came from — which page, which sentence. Set citations: {"enabled": True} on the document block. Claude then splits its reply into multiple text blocks, and blocks that draw on the document carry a citations array — each citation names the source, the cited text, and a location (a page number for PDFs, a character range for plain text). This is REAL SDK code.
citations.py# needs: pip install anthropic + ANTHROPIC_API_KEY (+ a real image/PDF file)
import base64
import anthropic
client = anthropic.Anthropic()
with open("report.pdf", "rb") as f:
pdf_b64 = base64.standard_b64encode(f.read()).decode("utf-8")
resp = client.messages.create(
model="claude-opus-4-8",
max_tokens=1024,
messages=[{
"role": "user",
"content": [
{
"type": "document",
"source": {"type": "base64", "media_type": "application/pdf", "data": pdf_b64},
"title": "Q4 Report",
"citations": {"enabled": True}, # turn citations on
},
{"type": "text", "text": "What were Q4 revenues? Cite the page."},
],
}],
)
for block in resp.content:
if block.type != "text":
continue
print(block.text)
for c in (block.citations or []):
# PDF citations carry a page_location; plain text carries char indices.
print(f' ↳ cited "{c.cited_text[:50]}" from {c.document_title}')
This recipe makes the answer traceable: with citations on, Claude tells you which page and sentence each claim came from. Real API code (▶ needs API key + network).
"citations": {"enabled": True}on thedocumentblock is the only new ingredient — everything else is the PDF recipe from earlier.- The reply is split into multiple
textblocks. A block that drew on the document carries acitationsarray; a block that didn't has none — henceblock.citations or []. - Each citation names the source (
document_title), the exactcited_text, and a location — a page number for PDFs, a character range for plain text.
Try this: Ask a question whose answer spans two pages and watch multiple citations come back. Note you can't combine citations with output_config.format (JSON schema) — the API returns a 400 if you try both.
citations on a document with output_config.format (JSON schema output) — the API returns a 400. Pick one: cite the source, or force a JSON shape. Citations are also all-or-none across a request's documents — enable them on every document block or none.8 · Cost, limits & production hygiene professional
Images and documents are billed as input tokens — a page or an image costs roughly what its visual complexity works out to, and a full-resolution image can be thousands of tokens. That means the recurring cost lever is how often you re-send the same bytes. The production checklist:
Production checklist for files & vision
- Don't re-inline. If a document is used more than once, upload it via the Files API and pass the
file_id— you stop paying to ship the bytes each call. - Cache big prefixes. Combine the Files API with
cache_controlso a large reused document is neither re-uploaded nor re-tokenized (see AP2). - Downsample when you can. A smaller image is fewer input tokens; send the resolution the task actually needs, not the original camera dump.
- Mind the quotas. Files API storage is capped per organization (100 GB) and individual files up to 500 MB — track what you upload.
- Clean up. Uploaded files persist until you
deletethem. Delete files you no longer need so storage doesn't creep and stale documents don't linger. - Count before you send. Use token counting (see AP4) on representative files to budget cost before rolling out.
files.list() entries.9 · Tech-lead — a document-ingestion contract tech-lead
A lead owns the document-handling policy for the whole system, not just one call site. That means a written contract: which paths inline vs upload (a size/reuse threshold, not per-developer whim); a file lifecycle (who uploads, who deletes, what the retention window is) so the org quota never becomes an incident; a citation requirement for any answer users act on, so claims are traceable to the source page; and a cost model that accounts for image/document input tokens and the caching + Files-API levers that reduce them.
Every mechanic in this lesson maps to one clause of that contract. The inline vs Files API table is the routing rule; the cleanup step is the lifecycle; citations are the traceability requirement; token cost is the budget. A lead makes these decisions once, writes them down, and lets the codebase follow the contract instead of re-litigating "inline or upload?" on every PR.
Exercise AP3.1 — Run the offline block builder, then wire a real call
Context: Knowing exactly what an image content block looks like — before any network call — demystifies vision requests. Matching the offline shape to the real block is the bridge from theory to a working call.
Your task: Run block_shape.py and confirm its printed shape matches the out() block, then point image_inline.py at a real PNG, add ANTHROPIC_API_KEY, and ask Claude what is in the image.
Requirements:
- First confirm the offline-printed block shape matches the expected output
- Point
image_inline.pyat a real PNG you have - Set
ANTHROPIC_API_KEYin your environment before the real call - Get Claude's description of the image back
- Note which parts of the real block are identical to the offline helper's output
💡 Hint: The base64 data string is the only part that changes between the offline shape and the real call — the surrounding block structure is identical.
Exercise AP3.2 — One document, inline vs Files API
Context: Asking one PDF three questions two ways makes the Files API's payoff concrete: you can watch the bytes-on-the-wire drop while comparing input-token cost. Adding citations shows the answers stay auditable either way.
Your task: Ask a multi-page PDF three questions inline (pdf_inline.py) and again by uploading once and referencing file_id (files_api.py), compare bytes and cost, then enable citations and delete the upload.
Requirements:
- Run the three questions both ways: inline base64 per call, and one upload referenced three times
- Compare the bytes sent and the input-token cost between the two approaches
- Enable citations (
citations.py) and confirm each answer names its page - Finish by
delete-ing the uploaded file
💡 Hint: The inline path re-sends the full base64 on every call; the Files API path sends the bytes once — that difference is exactly what you are measuring.
🪜 Practice ladder beginner → industry
Six graded exercises, easy to real-world. Try each before opening its solution.
Context: The simplest way to put an image in front of Claude is to inline it as base64 in an image content block. Getting the block shape right is the foundation for every vision task that follows.
Your task: Read a PNG, base64-encode it, and ask Claude what it shows using an inline image content block.
Requirements:
- Needs a real API key to run
- The base64 payload has no embedded newlines
- Use an
imageblock with a base64 source and matchingmedia_type(e.g.image/png) - The image block precedes the text block in the same user message
- Print the model's text answer describing the image
💡 Hint: Encode the raw bytes with base64.standard_b64encode(...).decode() and make media_type match the actual file type.
Show solution
The canonical inline-image block (base64 must have no newlines):
import base64, anthropic
client = anthropic.Anthropic()
with open("chart.png", "rb") as f:
data = base64.standard_b64encode(f.read()).decode("utf-8")
resp = client.messages.create(
model="claude-opus-4-8", max_tokens=512,
messages=[{"role": "user", "content": [
{"type": "image", "source": {
"type": "base64", "media_type": "image/png", "data": data}},
{"type": "text", "text": "What does this chart show?"},
]}],
)
print(next(b.text for b in resp.content if b.type == "text"))
Inline base64 is the simplest path for a one-off image. The image block comes before the text block, and media_type must match the file.
Context: PDFs go through a document block rather than an image block, and the API enforces hard size and page limits. Knowing those limits tells you when inlining stops being viable.
Your task: Send a PDF as a base64 document block, ask a question about it, and note the size and page limits.
Requirements:
- Needs a real API key to run
- Use a
documentblock with a base64 source andapplication/pdfmedia type - The document block comes before the text block
- No beta header is needed for base64 PDF
- State the limits: 32 MB per request and 600 pages (100 on 200k-context models)
- Note that a large or reused file should instead go through the Files API
💡 Hint: The block mirrors the inline-image pattern — swap image for document and use the PDF media type.
Show solution
PDF uses a document block, placed before the text:
import base64, anthropic
client = anthropic.Anthropic()
with open("report.pdf", "rb") as f:
pdf = base64.standard_b64encode(f.read()).decode("utf-8")
resp = client.messages.create(
model="claude-opus-4-8", max_tokens=1024,
messages=[{"role": "user", "content": [
{"type": "document", "source": {
"type": "base64", "media_type": "application/pdf", "data": pdf}},
{"type": "text", "text": "What are the three key findings?"},
]}],
)
print(next(b.text for b in resp.content if b.type == "text"))
Base64 PDF has no beta header. Limits: 32 MB per request and 600 pages (100 on 200k-context models). For a bigger or reused file, upload it once with the Files API instead (next rung).
Context: When the same document is queried repeatedly, re-inlining its bytes on every call is wasteful. The Files API lets you upload once and reference the file by id, paying the transfer a single time.
Your task: Upload a PDF once, then ask three questions referencing it by file_id without re-uploading, and clean up afterward.
Requirements:
- Needs a real API key to run
- Upload via
client.beta.files.uploadand reference the returnedfile_id - Include the
files-api-2025-04-14beta header on upload and on each message call - Reference the document by
{"type": "file", "file_id": ...}rather than re-sending bytes - Delete the uploaded file at the end (files persist until deleted)
- Note that uploads/lists/deletes are free but the content still bills as input tokens each use
💡 Hint: Do the upload once outside the question loop; inside the loop only the question text and the file_id reference change.
Show solution
Upload → reference by file_id → clean up. Beta header required on both:
uploaded = client.beta.files.upload(
file=("contract.pdf", open("contract.pdf", "rb"), "application/pdf"),
)
for q in ["Termination clause?", "Payment schedule?", "Governing law?"]:
resp = client.beta.messages.create(
model="claude-opus-4-8", max_tokens=1024,
betas=["files-api-2025-04-14"], # required on upload AND here
messages=[{"role": "user", "content": [
{"type": "text", "text": q},
{"type": "document",
"source": {"type": "file", "file_id": uploaded.id}},
]}],
)
print(q, "->", next(b.text for b in resp.content if b.type == "text")[:60])
client.beta.files.delete(uploaded.id) # files persist until deleted
The Files API avoids re-uploading the same bytes for every question — you pay the upload once and reference file_id thereafter. Uploads/lists/deletes are free; the content is billed as input tokens each time it is used.
Context: Image token cost scales with pixel area, so an oversized image can quietly dominate a request's bill. Estimating the cost before the call lets you trade fidelity for price deliberately.
Your task: Write image_tokens(w, h) and image_cost(w, h) for Opus 4.8 input, then compare a 1568×1568 image against a 784×784 downsize.
Requirements:
- Pure offline estimator — no API key
- Estimate tokens as roughly
(width × height) / 750 - Cost is the estimated tokens at the Opus 4.8 input price
- Report the percentage saved by halving each edge
- Show that halving both edges cuts cost to about a quarter (~75% saved)
- Note when to keep full resolution: dense documents or fine detail
💡 Hint: Because area is the driver, halving width and height quarters the pixel count — compute both sizes and diff the costs.
Show solution
Offline estimator — decide fidelity vs cost before the call:
IN = 5.0 / 1e6 # $ per input token, Opus 4.8
def image_tokens(w, h):
return round(w * h / 750) # Anthropic's rough estimate
def image_cost(w, h):
return image_tokens(w, h) * IN
print(image_tokens(1568, 1568), f"${image_cost(1568,1568):.5f}") # ~3279 $0.01640
print(image_tokens(784, 784), f"${image_cost(784, 784):.5f}") # ~820 $0.00410
print(f"downsizing saves {100*(1-image_cost(784,784)/image_cost(1568,1568)):.0f}%") # 75%
Token cost scales with pixel area, so halving each edge quarters the cost. Downsize client-side when the extra resolution will not change the answer — but keep full resolution for dense documents or fine detail where it will.
Context: For auditable answers, each claim should point back to a locatable span in the source document. Enabling citations turns a document answer into cited blocks you can verify.
Your task: Enable citations on a document so each cited answer block carries a source span, and show both the request flag and how to read the response citations.
Requirements:
- Needs a real API key to run
- Set
citations: {"enabled": True}on the document block - Citations are all-or-none across documents in the request
- Read each text block's
citationsforcited_textand a location - Handle the location type: page for PDFs, character range for plain text
- Note that citations are incompatible with forcing a JSON
output_config.format
💡 Hint: Iterate the response content blocks; each text block exposes a citations list whose entries name the cited text and its page or char range.
Show solution
Set citations.enabled on the document; the response splits into cited blocks:
resp = client.messages.create(
model="claude-opus-4-8", max_tokens=1024,
messages=[{"role": "user", "content": [
{"type": "document",
"source": {"type": "base64", "media_type": "application/pdf", "data": pdf},
"title": "Q4 Report",
"citations": {"enabled": True}}, # all-or-none across documents
{"type": "text", "text": "What was Q4 revenue? Cite the source."},
]}],
)
for block in resp.content:
if block.type == "text":
print(block.text)
for c in (block.citations or []):
# PDF -> page_location; plain text -> char_location
print(" cite:", c.cited_text, "p", getattr(c, "start_page_number", None))
Citations make answers auditable: each cited text block carries cited_text plus a location (page for PDFs, char range for text). Note citations are incompatible with output_config.format — you cannot force JSON and cite at the same time.
Context: A platform serving many documents needs one rule that decides transport and rejects over-limit files before a request is ever built. Encoding that contract as a gate keeps hard limits from failing deep in the call.
Your task: Write ingest_decision(size_mb, pages, reuse_count) that returns the transport (inline vs Files API) or a rejection.
Requirements:
- Pure decision logic — no API key needed
- Reject documents over 32 MB and over 600 pages, fast, before building a request
- Route reused documents (reuse_count ≥ 2) to the Files API
- Route one-off documents to inline base64
- Rejections explain the limit hit and advise splitting or chunking
- Never silently truncate an over-limit document
💡 Hint: Check the hard limits first and return early on rejection; only then branch on reuse count to pick inline versus Files API.
Show solution
Encode the ingestion contract as a gate (pure logic, no API needed):
def ingest_decision(size_mb, pages, reuse_count):
if size_mb > 32:
return "REJECT — exceeds 32 MB request limit; split the document"
if pages > 600:
return "REJECT — exceeds 600-page limit; chunk by section"
if reuse_count >= 2:
return "Files API — upload once, reference by file_id (avoid re-upload)"
return "inline base64 — one-off use, simplest path"
print(ingest_decision(2, 40, 1)) # inline base64
print(ingest_decision(2, 40, 5)) # Files API (reused 5x)
print(ingest_decision(50, 40, 1)) # REJECT — too big
The contract fails fast on the hard limits (32 MB, 600 pages) and routes by reuse: one-off → inline, reused → Files API to stop paying repeated upload/transfer. Never silently truncate an over-limit document — reject and chunk it.
✓ Checkpoint — you can move on when you can…
- Send an image inline as a base64
imagecontent block. - Send a PDF as a
documentblock and do document Q&A. - Upload a file once and reference it by
file_idacross many calls. - Choose inline vs Files API by reuse count and file size.
- Enable citations and read the source location back from the response.
- Explain the token cost of files and the cleanup/quota discipline they demand.
Knowledge check check yourself
When should you use the Files API instead of inlining base64, and what discipline does the Files API require that inlining does not?
Show answer
file_id rather than re-sending bytes. It requires lifecycle discipline: uploaded files persist indefinitely against a 100 GB org quota until you explicitly call files.delete().Why can't you combine document citations with output_config.format in the same request, and what does enabling citations change about the response shape?
Show answer
citations splits the response into multiple text blocks where citing blocks carry a citations array with document_title, cited_text, and a location (page number for PDFs, character range for text), so answers become traceable to their source.