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
PDF { 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

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:

A good import flow: extract, show an editable review table with the warnings, then save only what the person confirms.

Errors

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.