Building Features

Starlens: OCR and document extraction

Read receipts, invoices and other documents into the fields your app needs, and handle what Starlens can't read.

Starlens reads photos, PDFs, audio, and video into the fields your app needs. Use it for receipts, invoices, forms, and other documents.

What makes it useful

Starlens reads document structure as well as characters. It keeps table rows together, returns amounts as numbers, and normalizes dates (including Thai Buddhist-era dates). It can read handwriting and pages with multiple languages. You choose the fields; Starlens returns them in your schema.

How is it different from OCR?

Basic OCR returns text: TOTAL ฿482.00. Starlens can return total: 482 in a structured receipt, alongside the merchant and line items you asked for.

Basic OCRStarlens
OutputText for your app to parseFields and rows in your schema
Unreadable valueYour app must spot what's missingnull plus a reason in issues
Wrong documentReturns any visible textRefuses to extract it into the requested shape

Starlens can read multiple pages and documents in different languages. It keeps extracted names in their original script. It does not match printed names to your app's records or create those records.

Input and output examples

These illustrative requests use the SDK's actual input shape: inputs for the file, instructions for document context, and a Zod schema with field descriptions. The outputs below show output and issues; the SDK also returns version and usage.

Receipt input: A phone photo shows ร้านครัวบ้าน, 3 ก.ย. 2569, ข้าวผัด 2 × ฿60, and รวม ฿120. receiptUrl is its signed file URL:

import { createStarlensClient } from "@stardeck-customer-apps/starlens";
import { z } from "zod";

const starlens = createStarlensClient();
const receipt = await starlens.extract({
  version: "v2",
  inputs: [{ url: receiptUrl, filename: "receipt.jpg" }],
  instructions: "Purchase receipt. Copy printed values; do not calculate totals.",
  schema: z.object({
    merchant: z.string().nullable().describe("Shop name at the top, not the payment processor"),
    date: z
      .string()
      .nullable()
      .describe("Purchase date as YYYY-MM-DD; convert Thai Buddhist years"),
    line_items: z.array(
      z.object({
        description: z.string().nullable().describe("Priced item name, not a barcode-only line"),
        quantity: z.number().nullable().describe("Printed quantity"),
        amount: z.number().nullable().describe("Printed line amount"),
      })
    ),
    total: z
      .number()
      .nullable()
      .describe("Printed amount due / grand total / รวม, not a calculated sum"),
  }),
});

Output:

{
  "output": {
    "merchant": "ร้านครัวบ้าน",
    "date": "2026-09-03",
    "line_items": [{ "description": "ข้าวผัด", "quantity": 2, "amount": 120 }],
    "total": 120
  },
  "issues": []
}

Invoice input: A photo shows supplier Acme Foods and total ฿1,250, but a thumb covers the tax ID. invoiceUrl is its signed file URL. The field descriptions give Starlens the printed labels and disambiguation:

const invoice = await starlens.extract({
  version: "v2",
  inputs: [{ url: invoiceUrl, filename: "invoice.jpg" }],
  instructions: "Supplier invoice. Copy printed values; do not calculate totals.",
  schema: z.object({
    supplier: z.string().nullable().describe("Seller's legal entity name, not a brand or logo"),
    tax_id: z
      .string()
      .nullable()
      .describe("Printed Tax ID / TIN / VAT no. / เลขประจำตัวผู้เสียภาษี"),
    total: z.number().nullable().describe("Printed amount due / grand total / รวมทั้งสิ้น"),
  }),
});

Output:

{
  "output": { "supplier": "Acme Foods", "tax_id": null, "total": 1250 },
  "issues": [{ "path": "tax_id", "reason": "A thumb covers the tax ID" }]
}

Add it to your app

Ask your coding agent to build the scan and review flow. It uses the @stardeck-customer-apps/starlens package and its SKILL.md. In the browser, ask Starcat Developer in the app's Build tab instead. For example:

Add supplier bill scanning. Let staff photograph or upload a bill, then use Starlens
to read the supplier, date, line items, tax, and total. Show the image beside an
editable draft. Highlight unreadable fields, and require review before saving.
Reject files that are not supplier bills.

Your app runs Starlens. Settings → Services → Starlens shows past runs, their output, issues, and cost; it is not a place to start a scan. Source files are not stored in the run history.

Handle uncertain results

If Starlens cannot read a field, it returns null and an issues entry with the field and reason. Ask for a clearer photo or let someone correct the draft from the source. A null without an issue can mean the field was absent.

An empty issues list is not a confidence guarantee. Check important amounts and IDs against the source before saving. If Starlens refuses the whole file, ask for the right document; do not save an empty record.

Test with a playground

Ask your agent to build an extraction or eval playground in your app:

Build a Starlens playground for supplier bills. Upload a sample, run the same
schema and version as the real scan flow, and show the source beside the output
and issues. Let me enter correct values and compare them field by field. Do not
save playground results as supplier bills.

Try clear, blurry, covered, multi-page, and wrong-type samples. Improve the schema's field descriptions, then rerun the same samples to see what changed. Playground runs count toward your AI usage and the daily limit.

Known values and effort (v2)

If a field holds one of a list you already have, such as product names or staff names, pass the list as the field's examples. Starlens reads it as the values the field is likely to hold. The list is a hint, not a filter. A value that matches none of the list is transcribed as written.

const sheetSchema = z.object({
  rows: z.array(
    z.object({
      product: z
        .string()
        .nullable()
        .describe("Product name as written")
        .meta({ examples: productNames }),
      qty: z.number().nullable(),
    })
  ),
});

effort: "low" is the default and reads the document once. effort: "high" reads it three times and returns only the values all three reads agree on. It costs about four times as much and can take up to five minutes, so run it from a "Read again carefully" action, or in the background. High reads crops of images, so for a PDF, audio, or video input, Starlens runs low. Check result.effort before you tell a user that a document was read carefully.

For developers

Call @stardeck-customer-apps/starlens from server code with a pinned version (v2 for new code), a schema, and either publicly fetchable signed URLs or inline file bytes. The platform accepts up to 10 inputs and 40 MB total per request. The SDK returns output and issues, and raises UnsupportedDocumentError for a wrong document type. For the full API and error handling, see the package's SKILL.md in your app and The SDKs.