Oaklens: read images and documents
Turn photos, screenshots, PDFs, and pasted text into clean, structured data. Products from a photo of boxes or a pre-order email, line items from a receipt, people from business cards, or any shape you describe with your own JSON Schema. Reading is done by Claude (Anthropic's model). Anything unclear comes back as a warning, so a person can review before the data is saved.
Quick start
import { Oakstack } from "oakstack";
const oakstack = new Oakstack();
const result = await oakstack.lens.extract({
template: "product-list",
inputs: [
{ url: "https://yourapp.com/uploads/boxes.jpg" }, // a public image or PDF URL
{ data: pdfBytes, filename: "preorders.pdf" }, // bytes (Uint8Array) or base64
{ text: "2x Combat Patrol: Grey Knights $160\nCodex Grey Knights $60" },
],
instructions: "Prices are in USD. Mark anything listed as a pre-order.",
});
result.data.items; // [{ name, brand, quantity, condition, price, preorder, releaseDate, confidence, ... }]
result.warnings; // ["Release date has no year; 2026 assumed.", ...]
extract waits for the answer: usually 5 to 30 seconds, depending on how many pages you send. With curl:
curl https://oakstack.dev/api/v1/lens/extractions \
-H "Authorization: Bearer $OAKSTACK_API_KEY" \
-H "Content-Type: application/json" \
-d '{"template":"receipt","inputs":[{"url":"https://example.com/receipt.jpg"}]}'
Or with an agent: the MCP tools lens_list_templates and lens_extract.
Inputs
Up to 10 per request, mixed freely:
| Input | How to send | Limits | Pages counted |
|---|---|---|---|
| Image | { data } (bytes or base64) or { url } |
JPEG, PNG, GIF, WebP; 5 MB each | 1 |
{ data } or { url } |
10 MB, 30 pages each | its page count | |
| Text | { text } (a list, an email body, CSV, HTML) |
200,000 characters | 1 per 10,000 characters |
At most 60 pages per request. The file type is detected from the bytes, so mediaType is optional. URLs must be public https:// addresses (no localhost or private networks). iPhone HEIC photos aren't supported yet: send JPEG or PNG (screenshots are PNG already). Inputs are never stored; only the extracted result is kept, for your plan's log retention.
Templates
const templates = await oakstack.lens.templates.list(); // each with its JSON Schema
product-list: every distinct product from a photo of boxes or shelves, a screenshot, a pre-order email, or a pasted list. Fields:name,brand,productLine,sku,barcode,quantity,condition(new_sealed,new_open,used_complete,used_incomplete,damaged),price,currency,preorder,releaseDate,notes,confidence(0 to 1), plussourceKindand a one-linesummary. Identical boxes are counted, duplicates combined.receipt:merchant,date,currency,items(description,quantity,unitPrice,total),subtotal,tax,total,paymentMethod.contacts:contactswithname,company,title,emails,phones,address,website.
Values that aren't visible or stated come back as null: the model is told never to invent SKUs, barcodes, or prices.
Your own schema
Pass any JSON Schema with "type": "object" instead of a template:
await oakstack.lens.extract({
schema: {
type: "object",
properties: {
orderNumber: { type: ["string", "null"] },
lines: {
type: "array",
items: { type: "object", properties: { sku: { type: "string" }, qty: { type: "number" } } },
},
},
required: ["orderNumber", "lines"],
},
inputs: [{ url: "https://example.com/packing-slip.pdf" }],
});
Objects are treated as closed (no extra fields). A few validation keywords (minimum, pattern, maxLength, ...) are ignored, so check values in your code. Up to 20,000 characters of schema.
Reviewing results
Treat the output like a fast, careful assistant's first draft:
warnings: everything the model was unsure about (blurry, cut off, ambiguous quantities, a year it had to assume). Show them to the person importing the data.confidence(product-list): low values deserve a second look.- Text inside images or documents that looks like instructions ("ignore the above...") is read as data, never followed, and usually reported as a warning.
A good import flow: extract, show an editable review table with the warnings, then save only what the person confirms.
Errors
400 invalid_request: a bad input (wrong type, too big, unreadable URL, HEIC) or schema; the message names it. Nothing is counted.422 invalid_request: the model couldn't read the input. Not counted.429 usage_limit_reached: the monthly page limit is used up.429 rate_limited: too many extractions at once for your plan (the SDK retries).503: the reading service is busy. Not counted; the SDK retries.
Limits
| Plan | Pages a month | At once |
|---|---|---|
| Free | 20 | 1 |
| Builder | 200 | 3 |
| Studio | 500 | 6 |
Need more? A Reading pack add-on adds 1,000 pages a month for $25 (Billing page; stack as many as you need).
A page that produces a very long result (a dense list of dozens of products) counts as more than one: every 1,500 words of output is a page, like a long text counts as several SMS segments. The read's pages shows what was counted.
API reference
| Method | Path | What it does |
|---|---|---|
POST |
/api/v1/lens/extractions |
Read inputs. Body: inputs, template or schema, optional instructions (2,000 characters), metadata. |
GET |
/api/v1/lens/extractions |
Recent extractions (?limit=, max 100). |
GET |
/api/v1/lens/extractions/:id |
One extraction. |
GET |
/api/v1/lens/templates |
Templates with their JSON Schema. |