# 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

```ts
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:

```sh
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

```ts
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), plus `sourceKind` and a one-line `summary`. Identical boxes are counted, duplicates combined.
- **`receipt`**: `merchant`, `date`, `currency`, `items` (`description`, `quantity`, `unitPrice`, `total`), `subtotal`, `tax`, `total`, `paymentMethod`.
- **`contacts`**: `contacts` with `name`, `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:

```ts
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.                                                                            |
