How the work gets done

Document Extractor

Turns an invoice, packing list, or bill of lading into structured, reviewable line items.

4 min readUpdated Aug 6, 2026

Single-purpose LLM agent (vision). Turns a PDF or image of a customs document into structured fields, ready for downstream agents.

What it does

Reads a commercial invoice (PDF or image) and returns a Zod-validated structure: invoice number and date, supplier (name + country), buyer, currency and total, PO/reference numbers, vessel/voyage and bill-of-lading number if printed, per-line items (description, SKU, declared HTS as printed, quantity, unit of measure, unit price, line total), and free-text broker notes. This is the input every other agent depends on — if extraction fails, nothing downstream runs. (Bill-of-lading extraction is a separate extractor; non-invoice inputs are flagged, see below.)

Inputs

{
  filename: string;                 // "INV-22041.pdf"
  mimeType: string;                 // "application/pdf" | "image/png" | …
  bytes: Buffer;                    // raw file
  source?: 'upload' | 'inbox' | 'po-portal';
}

Extraction is cost-tiered, text-first. For a digital PDF with a real text layer, the text is pulled from the PDF and extracted cheaply on Claude Haiku (the default path). If that result fails a completeness gate — or the input is a scan, an image, or a thin-text PDF — it escalates to Claude Sonnet reading the layout-aware vision block (document/image content, no separate OCR), which is accurate for tables and line items. Same schema and prompt on either tier; only the model and input differ, and the tier that fired is reported in the result meta.

Outputs

{
  invoice_number: 'INV-22041',       // required
  invoice_date: '2026-05-24',        // ISO 8601, or null
  supplier_name: 'Hangzhou Lanyu Textile Co., Ltd.',  // or null
  supplier_country: 'CN',            // or null
  buyer_name: 'Northwell Outfitters',// importer / bill-to, or null
  currency: 'USD',                   // ISO 4217, or null
  total: 12_440.00,                  // document currency, decimals, or null
  references: ['PO-88231'],          // PO / contract numbers
  vessel: 'ONE TRIUMPH / 071E',      // carrier vessel + voyage if printed, or null
  bill_of_lading: 'ONEYSHA1234567',  // master/house B/L if printed, or null
  lines: [
    {
      line_number: 1,
      description: "Men's 100% cotton crew-neck T-shirts, knit, size M",
      sku: 'LY-CT-M',                // or null
      declared_hts: '6109.10',       // VERBATIM as printed, or null — never inferred
      quantity: 12000,
      unit_of_measure: 'EA',
      unit_price: 1.00,              // document currency, decimals, or null
      line_total: 12_000.00,
    },
    // … more lines
  ],
  notes: ['Incoterms: FOB Ningbo', 'Country of origin: CN'],  // free-text broker flags
}

The structure is flat: amounts are in document-currency decimals (not cents), and Incoterms, ports/origin, mode, and country-of-origin notes are surfaced as free-text notes rather than typed fields — the customs-clearance module derives ports, consignee, and per-line country-of-origin downstream when it builds the draft shipment.

Schema validation. Most fields are nullable by design: the model leaves what it can't read as null rather than inventing a value to satisfy the structure. A few identifying fields are required and non-null — invoice_number, and each line's description, quantity, unit_of_measure, and line_total. If the document isn't a commercial invoice at all, the model returns invoice_number: 'NOT_AN_INVOICE' and explains in notes[0] what it actually saw, rather than forcing a wrong parse. Every extracted field is broker-reviewable in the drawer, and missing fields are flagged by downstream services so the broker knows what to ask the supplier for.

When it runs

  • Drag-drop upload on Dashboard / Simulation / New entry wizard (upload source) → fires immediately, surfaces the structured result in the inbox row.
  • Email inbox parse — when a packet arrives, every attachment runs through the extractor on receipt.
  • Re-extract on demand — broker clicks "Re-parse" in the drawer when the source PDF is updated.

Dependencies

  • Two models: Claude Haiku over the extracted PDF text (default, cheap tier) and a vision-capable Claude Sonnet that accepts the PDF or image directly (escalation tier for scans/images/thin-text).
  • PDF text extraction for the text-first tier; no third-party OCR — the vision tier reads the layout natively when text isn't available.
  • No retrieval, no DB lookups at extract time. Document-to-structure only. The HTS hint surfaced from the document is only a hint; the classifier re-runs from the description regardless.

What success looks like

High-accuracy structured extraction across the fields a broker needs to open an entry — document type, parties, ports, dates, line items with quantities and values, and totals — from real commercial invoices, packing lists, bills of lading, and POs. Every field is surfaced for review rather than trusted blindly: we do not auto-file based on extracted data alone. The broker reviews every field in the drawer before transmit.

What it does NOT do

  • Does not invent values. Unreadable fields come back null (most fields are nullable; only a few identifying fields are required), and a non-invoice returns the NOT_AN_INVOICE sentinel instead of a forced parse. The drawer shows missing fields to the broker as "Missing — broker to confirm".
  • Does not classify HTS codes. It surfaces any HS hint printed on the document, but the HTS Classifier re-runs from the description for every line regardless.
  • Does not translate documents. Foreign-language invoices are extracted in the original language; translation is a separate concern.
  • Does not read handwriting reliably. Scanned and printed text only. Handwritten amendments are flagged for broker review.
  • Does not stitch multi-document packets (invoice + packing list + BoL) into a single record. Each document is extracted independently; the customs-clearance module reconciles them at the entry level.

Roadmap

  • Q3 2026 — Multi-document packet reconciliation: when invoice + packing list + BoL arrive together, merge into one entry draft so the broker isn't reconciling line numbers manually.
  • Q3 2026 — Handle handwritten amendments by surfacing the raw region to the broker instead of guessing.
  • Q4 2026 — Auto-detect when a document is a revised version of one already in the inbox (same supplier + invoice number, different totals) and prompt the broker to diff.
  • 2027 — Language hints: detect non-English documents and surface a "translate" affordance in the drawer for the broker.
Ready to see it live?

Put your own book of entries through Aduaria.