LM-Kit OneDocs2026.8.10lm-kit.com
Solutions/The Cookbook

Invoice Automation, End to End

The founding IDP workload as a runnable sequence: a multi-document scan arrives, and validated, typed records leave, with humans seeing only the documents that earned their attention. Four calls per batch plus one per document, all conventions as stated in the cookbook hub.


1Upload the batch, split it into labeled documents#

Upload the scan (files/upload, hub convention), then split with physical output, so each detected document comes back as its own file id, ready to chain. This step is job-friendly on large batches.

curl -s "$LMKIT_URL/lmkit/v1/document-splitting" \
  -H "Authorization: Bearer $LMKIT_API_KEY" -H "Content-Type: application/json" \
  -d '{
    "input": "'$BATCH_FILE_ID'",
    "input_format": "FileIdentifier",
    "split_documents": true,
    "guidance": "Each invoice or credit note is one document; remittance advice belongs with its invoice."
  }'
{
  "contains_multiple_documents": true,
  "document_count": 3,
  "confidence": 0.94,
  "segments": [
    { "start_page": 1, "end_page": 2, "page_count": 2, "label": "Invoice",     "file_id": "8a1c..." },
    { "start_page": 3, "end_page": 3, "page_count": 1, "label": "Credit Note", "file_id": "77e2..." },
    { "start_page": 4, "end_page": 6, "page_count": 3, "label": "Contract",    "file_id": "0f9d..." }
  ]
}

Branch here: low batch confidence routes the whole scan to a human sorter instead of proceeding on a bad split (Intelligent Splitting).

2Classify each segment against YOUR taxonomy#

The segment label is a useful prior; the classification is the decision. Descriptions are the accuracy lever, and unknown stays allowed so junk pages cannot force-file (Document Classification).

curl -s "$LMKIT_URL/lmkit/v1/categorize" \
  -H "Authorization: Bearer $LMKIT_API_KEY" -H "Content-Type: application/json" \
  -d '{
    "input": "'$SEGMENT_FILE_ID'",
    "input_format": "FileIdentifier",
    "categories": ["Invoice", "CreditNote", "Contract", "Correspondence"],
    "category_descriptions": [
      "A bill requesting payment: line items, totals, payment terms",
      "A negative adjustment referencing a prior invoice",
      "A signed agreement or its annexes",
      "Letters, emails, and anything that is none of the above"
    ],
    "allow_unknown_category": true
  }'
{ "categories": [ { "value": "Invoice" } ], "confidence": 0.97 }

Your code switches on the value: each class selects its own extraction schema, and an empty result (unknown) routes to a triage queue.

3Extract with the class's schema#

The invoice schema uses the schema dialect's guarantees: grammar-enforced enums and dates, entity validation triggered by field NAMES (iban, vat_number, total_amount), and include_elements for the per-field evidence. Job-friendly.

curl -s "$LMKIT_URL/lmkit/v1/extract-structured-data" \
  -H "Authorization: Bearer $LMKIT_API_KEY" -H "Content-Type: application/json" \
  -d '{
    "input": "'$SEGMENT_FILE_ID'",
    "input_format": "FileIdentifier",
    "include_elements": true,
    "jsonSchema": {
      "type": "object",
      "properties": {
        "invoice_number": { "type": "string", "trimStart": ["INV-", "INV"] },
        "issue_date":     { "type": "string", "format": "date" },
        "due_date":       { "type": "string", "format": "date" },
        "currency":       { "type": "string", "enum": ["EUR", "USD", "GBP", ""] },
        "vendor": {
          "type": "object",
          "properties": {
            "name":       { "type": "string" },
            "vat_number": { "type": "string" },
            "iban":       { "type": "string" }
          },
          "required": ["name"]
        },
        "line_items": {
          "type": "array",
          "items": {
            "type": "object",
            "properties": {
              "description": { "type": "string" },
              "quantity":    { "type": "number" },
              "unit_price":  { "type": "number" }
            },
            "required": ["description", "quantity", "unit_price"]
          }
        },
        "total_amount": { "type": "number", "description": "Grand total including tax." }
      },
      "required": ["invoice_number", "issue_date", "vendor", "total_amount"]
    }
  }'
{
  "json": "{ \"invoice_number\": \"2024-0117\", \"issue_date\": \"2024-03-02\", ... }",
  "confidence": 0.91,
  "human_verification_required": true,
  "elements": [
    { "path": "invoice_number", "confidence": 0.98, "validation": "not_applicable", "page_index": 0,
      "bounds": { "x0": 431.2, "y0": 88.1, "x1": 512.6, "y1": 88.1, "x2": 512.6, "y2": 101.4, "x3": 431.2, "y3": 101.4 } },
    { "path": "vendor.iban", "confidence": 0.99, "validation": "repaired",
      "entity_kind": "Iban", "original_value": "FR76 3000 4000 0312 3456 7890 K43", "page_index": 0, "bounds": { "x0": 60.0, "y0": 640.2, "x1": 268.9, "y1": 640.2, "x2": 268.9, "y2": 652.0, "x3": 60.0, "y3": 652.0 } },
    { "path": "due_date", "confidence": 0.44, "validation": "not_applicable",
      "human_verification_required": true, "page_index": 0, "bounds": null }
  ],
  "pages": [ { "index": 0, "width": 595.3, "height": 841.9, "unit": "pt" } ]
}

4Route on the flag, review with coordinates#

The branch that makes this a pipeline instead of a demo (Structured Extraction):

  • human_verification_required: false: parse json, post the record to your system. Straight-through, no human.
  • true: queue for review, showing ONLY the flagged elements. Render the page (the thumbnail endpoint), draw each flagged element's bounds (normalized by ITS page's width and height from pages), and show original_value next to the extracted value for repaired fields. The reviewer confirms in seconds because their eye is pointed at the exact spot.

Corrections you collect here are not waste: they become the labeled sample for measurement and, at volume, the training set for fine-tuning a compact model on your own invoices.

5Production notes#

  • Force async for the batch job. In production, run steps 1 and 3 with Prefer: respond-async and a polling worker; the jobs contract is the loop, and progress_current/progress_total drive the dashboard.
  • Handle the deterministic refusals. A password_required or unsupported_file_type failure is a routing decision, not a retry: send those to the same triage queue as unknown classifications.
  • Throughput is a setting. Segments classify and extract independently, so parallelize across the slot pool rather than serializing the batch.