Images and documents
Send screenshots, PDFs and charts as input.
Create a free account Try part of it here
Why it matters
A lot of useful information lives in pictures and PDFs. Think of screenshots of ad dashboards, a rival's landing page, charts in a report, invoices or scanned survey forms. Models that can read images and documents turn all of that into data your code can use. Nobody has to retype the numbers. And you send a file in the same kind of request you already know. It goes in as one more block in the message.
What you'll be able to do
- Send an image or a PDF to the model alongside a question
- Encode files as base64 and pick the right media type
- Estimate the tokens an image will use
- Spot good uses for vision in marketing work, and its limits
What it covers
- A message can hold more than text
- Base64: a file spelled out in letters
- PDFs are documents
- Good uses, and limits
- Worked example: send an image with a question
A message can hold more than text
Say you want a friend's opinion on a chart. You don't read the chart out over the phone. You hand them a printout, then ask your question. Sending an image to a model works the same way.
So far, every message's content has been a string. It can also be a list of blocks. A content block is one item in the message. Its type says what it holds: text, an image or a document. A model that can read more than text is called multimodal.
Here's a user message with two blocks. The image comes first, then the question:
message = {
"role": "user",
"content": [
{"type": "image", "source": {"type": "base64", "media_type": "image/png", "data": png_base64}},
{"type": "text", "text": "What's the click-through rate in this dashboard screenshot?"},
],
}
The image block has a source. It says what kind of file this is and holds the file itself. png_base64 stands in for the image's data. You'll see how to make it in a moment.
Two habits help:
- Put images and documents before the question. Models do best when they've "seen" the material before they read what to do with it. It's the printout first, then the question.
- Ask a specific question. "What's the CTR for the Spring Sale row?" beats "describe this". CTR, the click-through rate, is the share of people who saw an ad and clicked it.
An image is just another kind of block in the same message.
Base64: a file spelled out in letters
A file is made of bytes. Each byte is a number from 0 to 255. Many of those numbers aren't letters you could type. But a request body is JSON, and JSON can only carry text. So the bytes have to become text first.
Base64 does that job. It rewrites any file as a long string made from 64 safe characters: capital letters, small letters, digits, + and /. Picture reading a photo down the phone, one letter at a time. It's long and slow, but nothing gets lost. The other end can rebuild the exact file.
Python's base64 module does the work. The cell below holds a real image: a PNG one pixel wide and one pixel tall. Its bytes are written as hex, which shows each byte as two characters from 0–9 and a–f.
import base64 # The 70 bytes of a tiny 1×1 PNG image (one light-blue pixel), written out as hex. png_bytes = bytes.fromhex( "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c489" "0000000d49444154789c63f03ff4ff3f00068403100ad117f20000000049454e44ae426082" ) encoded = base64.b64encode(png_bytes).decode("ascii") print(len(png_bytes), "bytes became", len(encoded), "characters:") print(encoded)
The 70 bytes become 96 characters. The == at the end is padding. It fills out the last group.
Base64 always turns 3 bytes into 4 characters. So an encoded file is about a third bigger than the original.
In plain words: divide the file size by 3, then multiply by 4. A 30 KB screenshot becomes about 40 KB of text.
Check it on the tiny image. 70 ÷ 3 is about 23.3, and base64 rounds that up to 24 groups. Then 24 × 4 = 96 characters, the number the cell printed. Base64 adds size and never shrinks a file.
The extra size matters because the whole request has a size limit: 32 MB on the Claude API. In real code you'd read the file from disk: base64.b64encode(open("dashboard.png", "rb").read()).decode(). The "rb" means "read the raw bytes".
Go deeper Why exactly a third bigger?
Each byte holds 8 bits, the 0s and 1s a computer stores. Three bytes hold 3 × 8 = 24 bits. Base64 cuts those 24 bits into four groups of 6 bits. Six bits can make 64 different patterns, one for each character in the base64 alphabet. So every 3 bytes become 4 characters, and each character takes one byte to send. When a file's length isn't a multiple of 3, the last group is filled out with = signs.
Media types
The media_type tells the API what kind of file it is, like the label on a package. For images you can use image/png, image/jpeg, image/gif or image/webp. Images on the public web can be sent by address instead. Use {"type": "url", "url": "https://..."} as the source.
Show the answer
About 400 KB
Base64 turns every 3 bytes into 4 characters, so the text is about a third bigger: 300 ÷ 3 × 4 = 400 KB. It never compresses a file, so 200 KB is wrong.
PDFs are documents
PDFs go in a document block with "media_type": "application/pdf". The model sees both the text and the page images. So charts and tables inside the PDF are readable too:
{"type": "document", "source": {"type": "base64", "media_type": "application/pdf", "data": pdf_base64}}
On current Claude models, a single request can include up to 600 PDF pages. Models with smaller context windows take fewer. The 32 MB request limit still applies. For long reports, send only the pages you need. That's cheaper and often gives better answers.
What images cost
Images are turned into tokens like everything else, and you pay for those tokens. Here's a useful rule of thumb for Claude:
tokens ≈ (width in pixels × height in pixels) ÷ 750
A pixel is one dot of color on a screen. Width times height is the number of dots in the whole image. The sign ≈ means "about equal to".
In plain words: count the dots in the image, then divide by 750. Every 750 pixels cost about one token.
Here's a small example. A cropped chart is 300 pixels wide and 250 tall. That's 300 × 250 = 75,000 pixels, and 75,000 ÷ 750 = 100 tokens. A full 1,000 × 1,000 screenshot has 1,000,000 pixels. That comes to about 1,333 tokens.
In Python, it's one line per image. round gives the nearest whole number:
for width, height in [(300, 250), (1000, 1000), (1200, 800)]: print(width, "x", height, "is about", round(width * height / 750), "tokens")
It prints 100, 1333 and 1280 tokens. Treat these as estimates, not exact counts.
Very large images are scaled down before the model sees them. So the cost stops growing past a certain size. Check the vision docs for the current limits. The exercise uses a cutoff of 1,568 pixels on the longer side.
Two cheap habits keep image costs down:
- Crop screenshots to the part that matters.
- Don't send a 4K image when a 1,200-pixel one is as easy to read. A 4K image is about 3,840 pixels wide.
print(round(600 * 450 / 750))
Show the answer
360
600 × 450 = 270,000 pixels, and 270,000 ÷ 750 = 360 tokens. 1,050 adds the sides instead of multiplying them. 270,000 forgets to divide by 750.
Good uses, and limits
Strong uses for marketing work:
- pulling numbers out of dashboard screenshots and PDF reports into structured data
- critiquing ad creative or a landing page against your brand guidelines
- reading charts and summarizing trends
- extracting fields from invoices, receipts and forms
The same ideas work outside marketing. A shop owner could pull totals from a stack of receipts. A clinic could read the fields on scanned forms.
Limits to plan for:
- Tiny text and dense tables can be misread. Ask for structured output, such as JSON with named fields. Then spot-check some numbers against the image.
- Spatial questions are less reliable than reading text. "Which button is left of the logo?" is the kind of question models get wrong more often.
- Faces: models won't identify real people from their faces.
Show the answer
Which button sits to the left of the logo in a page screenshot
Questions about where things sit on the page are less reliable than reading text. Numbers can be misread too, which is why you ask for named fields and spot-check them.
Worked example: send an image with a question
Build a message with an image block and a question, then send it. The tiny 1×1 image stands in for a screenshot. Without an API key, the simulated reply ignores the image. But the request is exactly what you'd send for a real one.
import base64 import requests png_bytes = bytes.fromhex( "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c489" "0000000d49444154789c63f03ff4ff3f00068403100ad117f20000000049454e44ae426082" ) image = {"type": "image", "source": {"type": "base64", "media_type": "image/png", "data": base64.b64encode(png_bytes).decode("ascii")}} question = {"type": "text", "text": "What color is this image? Answer in one word."} response = requests.post( "https://api.anthropic.com/v1/messages", headers={"x-api-key": "added-by-the-academy-server", "anthropic-version": "2026-10-21"}, json={"model": "claude-opus-5-5", "max_tokens": 200, "messages": [{"role": "user", "content": [image, question]}]}, timeout=60, ) data = response.json() print(response.status_code, "".join(b["text"] for b in data["content"] if b["type"] == "text")[:100]) print("input tokens:", data["usage"]["input_tokens"])
The image block comes first and the question second, in the same user message. The data field holds the base64 string. With a real key, the model answers about the actual image. Here the simulated reply only quotes the text block. That's a good reminder: your code shouldn't count on a particular answer.
Show the answer
[image block, text block with a specific question]
Image first, then a specific question in the same message. A file name alone gives the model nothing to look at, and 'describe this' is too vague.
Practice what you just read
- 5 code examples you can edit and run
- a practice exercise that checks your code
- a 5-question quiz
- 4 review cards for spaced repetition
Try a quiz question
Why are images base64-encoded in API requests?