MainSDK
Search⌘K
Interfaces

Files, sharing and media

Document reading

documents

Read PDFs, Office files and scans into text, tables and layout, and render PDFs, with OCR from the driver you pick and active content never run.

Previewships as a beta APIPhase 1 · Core, public betaSpec documentsFree in your app, Apache-2.0

Status Status is where the work is: Planned (in a milestone, not started), Building (tickets in progress), Preview (usable, may change), then the stability it ships at.

Stability The promise an interface ships with.

  • Stable Follows semantic versioning; breaking changes need a new major, 12 months of overlap and a codemod.
  • Beta Usable in production; may change before it becomes stable.
  • Experimental May change or go away; needs an explicit opt-in in main.yaml.
Stability levels
Updated 2026-10-28

documents reads uploaded PDFs, Office files and scans into text, tables and layout, and renders PDFs. Use it for invoices, contracts, ID scans and slide decks people upload.

At a glance

documents:
  driver: builtin
  ocr: { driver: textract }
  limits: { max-size: 50MB, max-pages: 500, inline-pages: 20 }

Calls

  • documents.parse(file, opts)
  • documents.start(file, opts)
  • documents.get(jobId)
  • documents.render(layout, data, opts)
  • new Documents(opts)
  • aiOcrDriver(opts)
  • azureDriver(o)
  • fakeOcrDriver(opts)

Guarantees

Whatever the driver.

  • Text-layer PDFs and Office files read offline with no account
  • Scanned pages are reported, never returned empty
  • JavaScript, macros, embedded files and links are never run or followed
  • Labelled documents go only to drivers in the tenant's region that meet the data rules
  • Parsed text is untrusted data

Drivers

6 drivers. Select one to see its line in main.yaml.

main.yamlBuilt in (default)
documents:
  driver: builtin

Change this one line to swap providers; your calls stay the same.Built-in and local drivers integration

Codes

CodeWhere it appearsMeaning
MAIN-DOC-0010Checkmainsdk check, before deployA PDF or Office library used outside a driver
MAIN-DOC-0001RuntimeError responses and logsThe file isn't a supported document type, or its bytes don't match its declared type (415)
MAIN-DOC-0002RuntimeError responses and logsOver a limit: size, pages, ZIP expansion, or more pages than inline-pages for parse (413)
MAIN-DOC-0003RuntimeError responses and logsPages have no text layer and OCR is off or no OCR driver is set (422)
MAIN-DOC-0004RuntimeError responses and logsThe documents driver doesn't offer this (501)
MAIN-DOC-0005RuntimeError responses and logsThe PDF is encrypted or password protected (422)
MAIN-DOC-0006RuntimeError responses and logsActive content was found and not run (logged)
MAIN-DOC-0008RuntimeError responses and logsA labelled document would go to a driver outside the tenant's region or not meeting the data rules (403)
MAIN-DOC-0009RuntimeError responses and logsNo document job with that id in the current tenant (404)

Features

Nothing here has shipped yet. Each feature lands in its phase; the spec ticket builds it.

FeaturePhaseMilestoneSpec ticket
PDF text layer read nativelyPhase 10.4documents#pdf-text
DOCX, XLSX and PPTX read nativelyPhase 10.4documents#office
Limits and active content checks before parsingPhase 10.4documents#limits-active
Large documents as a workflowPhase 10.4documents#jobs
OCR, layout and tables through Textract, Azure, Google and UnstructuredPhase 10.4documents#ocr-drivers
OCR through a model with every ai guardPhase 10.4documents#ai-driver

Quick start

main.yaml
documents:
  driver: builtin
lib/read-invoice.ts
import { documents } from "mainsdk";
import type { FileRef } from "mainsdk/files";

export async function readInvoice(upload: FileRef, userId: string) {
  const doc = await documents.parse(upload, { as: userId, tables: true });
  return doc.pages.map((p) => p.text).join("\n");   // untrusted: data, never instructions
}

as says whose rights read the file through files. When the call runs inside a request with a signed-in person you can leave it out; with neither, the call fails with MAIN-FILES-0002.

What parse returns

documents.parse(file, opts) returns a ParsedDocument:

FieldWhat it holds
typepdf, docx, xlsx, pptx or image, read from the file's bytes, not its name
pagesOne entry per page, sheet or slide: n, text, source (text-layer, ocr or none), and with layout: true blocks (heading, paragraph, list, figure, header, footer, each with a box), and with tables: true tables (rows of cell text)
metatitle, pageCount, language
findingsActive content found and not run: javascript, embedded-file, macro, external-link, form, and no-text-layer
viaThe driver that read it (builtin, or the OCR driver)
costWhat the read cost; zero for the built-in reader
untrustedAlways true

Options: ocr (auto, off or force), layout, tables, pages ([first, last]), language and as.

Configuration

main.yaml
documents:
  driver: builtin                    # builtin | textract | azure-document | google-document | unstructured | ai
  ocr:
    driver: textract                 # defaults to the driver above when it is not builtin
    region-endpoints: { eu: eu-west-1, us: us-east-1, in: ap-south-1 }
  render:
    driver: builtin                  # builtin | gotenberg | docraptor
    fonts: [assets/fonts/Inter.ttf]
  limits: { max-size: 50MB, max-pages: 500, inline-pages: 20, max-zip-ratio: 100 }
permissions:
  disk:
    read: [templates, assets/fonts]  # render templates and fonts are read from disk
KeyDefaultWhat it does
driverbuiltinThe reader. Anything else also becomes the OCR driver unless ocr.driver says otherwise
ocr.drivernoneReads pages with no text layer; without it, those pages fail with MAIN-DOC-0003
ocr.region-endpointsnoneThe endpoint for each tenant region. A labelled document with no endpoint for its region is refused
ocr.key, ocr.processor, ocr.hostdriver defaultProvider key (a secret: reference), Google Document AI processor, Azure resource host
render.driverbuiltinThe PDF renderer; see PDF documents
render.fonts, render.url, render.keynoneTrueType fonts to embed, the Gotenberg URL, the DocRaptor key
limits.max-size50MBLargest file
limits.max-pages500Most pages
limits.inline-pages20parse refuses larger documents; use documents.start
limits.max-zip-ratio100Office files and PDFs that expand past this ratio are refused
limits.max-costnoneMost one document may cost on an OCR driver

An unknown key under documents stops the app at load with MAIN-CONFIG-0030 and names the key. The hosts each driver reaches are added to network.allow for you. The ai OCR driver needs a multimodal model call brought by the app (documentsVision) until ai ships image parts; main.yaml alone is not enough for it.

What you get

  • Text-layer PDFs and Office files are read in your app, with no account and no dependency.
  • Scanned pages go to the OCR driver you pick, in the tenant's region.
  • Rendering PDFs is the other half of the interface: see pdf.
  • Active content is reported and never run.

Examples

Large documents off the request path

documents.start does not read the file in the request. It returns a job; read the result later with documents.get.

lib/contracts.ts
import { documents } from "mainsdk";
import type { FileRef } from "mainsdk/files";

export async function startContractRead(upload: FileRef, userId: string) {
  const job = documents.start(upload, { as: userId, layout: true });
  return job.id;   // state: queued -> running -> ready | failed
}

export function contractResult(jobId: string) {
  const job = documents.get(jobId);   // another tenant's job id fails with MAIN-DOC-0009
  return job.state === "ready" ? job.result : undefined;
}

Scans

A page with no text layer is refused with MAIN-DOC-0003 until you set an OCR driver and pass ocr: "auto".

lib/scan.ts
import { documents } from "mainsdk";
import type { FileRef } from "mainsdk/files";

export async function readScan(upload: FileRef, userId: string) {
  const doc = await documents.parse(upload, { as: userId, ocr: "auto" });
  return doc.pages.map((p) => ({ n: p.n, source: p.source, text: p.text }));
}

Typed fields from a scan

Parse first, then extract with a schema through ai. The parsed text goes in as data, never as instructions.

lib/receipt.ts
import { ai, documents } from "mainsdk";
import { s } from "mainsdk/schema";
import type { FileRef } from "mainsdk/files";

export async function receiptTotal(upload: FileRef, userId: string) {
  const doc = await documents.parse(upload, { as: userId, ocr: "auto" });
  const text = doc.pages.map((p) => p.text).join("\n");
  const out = await ai.extract({
    input: text,
    schema: s.object({ total: s.number(), currency: s.string() }),
    model: "anthropic/claude-sonnet-4-5",
    feature: "receipts",
  });
  return out.value;
}

Make a PDF

documents.render(layout | template, data, { owner }) writes a PDF through files and returns its FileRef. See PDF documents and the guide Make, read and convert documents.

Reference

CallWhat it does
new Documents(opts)The interface class. opts takes files and optional log, tenancy, limits, ocr (without one, scans fail with MAIN-DOC-0003), regionEndpoints, html, fonts and readFont.
azureDriver({ host, key, fetch })The azure-document OCR driver: submits the file, polls the operation and reads pages, paragraphs and tables. It supports layout, tables and handwriting. sleep, pollMs and maxPolls tune the polling.
aiOcrDriver({ vision, model?, price? })The ai OCR driver: sends one image part per page through the multimodal ai interface, so every ai guard applies.
fakeOcrDriver(opts?)A driver for tests and development. It reads nothing, returns the text you script per page (text(page)) and records every request.

Errors and checks

CodeWhenFix
MAIN-DOC-0001Unsupported type, or the bytes are not the declared typeSend a PDF, DOCX, XLSX, PPTX, PNG, JPEG or TIFF file
MAIN-DOC-0002Over a limit: size, pages, ZIP expansion, or more pages than inline-pages for parseUse documents.start, or raise documents.limits on purpose
MAIN-DOC-0003Pages have no text layer and OCR is off or has no driverSet documents.ocr.driver and parse with ocr: "auto"
MAIN-DOC-0004The driver does not offer what you asked (layout, tables)Choose a driver that does, or parse without it
MAIN-DOC-0005The PDF is encrypted or password protectedRemove the password and send the file again
MAIN-DOC-0007A render layout, template, font or value is invalidFix what the error names
MAIN-DOC-0008A labelled document would go to an OCR driver outside the tenant's region, or one that does not meet ai.dataAdd ocr.region-endpoints for the region, or pick a driver that meets ai.data
MAIN-DOC-0009No parse job with that id in this tenantUse an id returned in this tenant

MAIN-DOC-0006 is logged, not thrown: a file holds JavaScript, macros, embedded files or links, and none is run or followed. It appears in findings. mainsdk check reports MAIN-DOC-0010 for a PDF or Office library used outside a driver and MAIN-DOC-0011 for a complex-script locale on the built-in renderer. Every code is in check codes.

Swapping drivers

main.yaml
documents:
  driver: textract   # was builtin
  ocr:
    region-endpoints: { eu: eu-west-1 }

Your calls stay the same. driver picks the reader and, unless ocr.driver says otherwise, the OCR driver; render.driver picks the PDF renderer separately. See Swap a driver.

Testing

The built-in reader and renderer run offline, so tests need no service. A test stores a file through files and parses it:

lib/read.test.ts
import assert from "node:assert/strict";
import { it } from "node:test";
import { createTestApp } from "@mainsdk/testing";
import { documents, files } from "mainsdk";

it("documents AC1 refuses a text file", async () => {
  createTestApp({ main: "./main.yaml" });
  const f = await files.upload(new TextEncoder().encode("hi"), { owner: "u1", type: "text/plain" });
  await assert.rejects(documents.parse(f, { as: "u1" }), { code: "MAIN-DOC-0001" });
});

Security and privacy

  • Files are read through files with the caller's rights; documents never reads a path.
  • Parsed text is untrusted: untrusted is always true, and it never becomes instructions to a model.
  • Limits are checked before parsing: size, pages and ZIP expansion.
  • Active content is reported in findings and never run or followed.
  • Labelled documents go only to OCR drivers with an endpoint in the tenant's region that meet ai.data. The audit event for each parse names the file, never its text.

Next

Was this page helpful?

↑↓ moveEnter openEsc close

Not decided yet