Use case

Invoice processing decisions with a fine-tuned decision model

Accounts payable spends less time on routine invoices and catches duplicates and mismatches before they are paid. After your OCR step pulls out the fields, Laya's typed-decisions checkpoint, fine-tuned on this workflow, decides whether the invoice matches the order, whether it is a duplicate, how serious any discrepancy is and whether to approve, hold or reject.

8 min readLast updated

Swiss-hosted inference. Nothing you send is ever stored.Swiss data residency

In 30 seconds

  • Extraction tools read the invoice; Laya makes the decision that follows.
  • Each invoice gets four answers: matches the order? duplicate? how serious is the gap? approve, hold or reject?
  • Confident, clean invoices move on; exceptions go to a clerk.
  • Invoice content is processed in Switzerland and not stored.
  • Accuracy was measured on a synthetic benchmark, so validate it on your own invoices.

Code and dense tables are folded away. Open any of them on demand.

Live demo · no signup

Try this use case

Edit the text if you like, then press run. Laya answers every question at once, with a probability for each option.

1 more question in the full request.567/600

The questions it answers

  • matches_orderyes / noDo the quantities and unit prices on `invoice` match `purchase_order` and `goods_receipt`?
  • duplicateyes / noIs `invoice` a duplicate of one in `recent_invoices_same_vendor`?
  • discrepancy_severityscoreHow severe is any discrepancy between `invoice` and `purchase_order`?
  • dispositionchoiceWhat should accounts payable do with `invoice`?

The answers appear here as bars: the longer the bar, the more likely Laya thinks that option is.

Show the full API request· JSON
POST https://api.laya.studio/v1/systemonejson
{
  "state": {
    "invoice": {
      "number": "INV-20931",
      "vendor": "Acme Industrial Supply",
      "date": "2026-09-15",
      "due": "2026-09-30",
      "currency": "EUR",
      "lines": [
        {
          "sku": "BRG-6204",
          "qty": 400,
          "unit_price": 3.1
        },
        {
          "sku": "SEAL-22",
          "qty": 200,
          "unit_price": 0.85
        }
      ],
      "total": 1410
    },
    "purchase_order": {
      "number": "PO-7781",
      "lines": [
        {
          "sku": "BRG-6204",
          "qty": 400,
          "unit_price": 2.9
        },
        {
          "sku": "SEAL-22",
          "qty": 200,
          "unit_price": 0.85
        }
      ],
      "total": 1330
    },
    "goods_receipt": {
      "received": [
        {
          "sku": "BRG-6204",
          "qty": 400
        },
        {
          "sku": "SEAL-22",
          "qty": 200
        }
      ]
    },
    "recent_invoices_same_vendor": [
      {
        "number": "INV-20877",
        "total": 980,
        "date": "2026-08-29"
      }
    ]
  },
  "questions": {
    "matches_order": {
      "type": "noul",
      "instructions": "Do the quantities and unit prices on `invoice` match `purchase_order` and `goods_receipt`?"
    },
    "duplicate": {
      "type": "noul",
      "instructions": "Is `invoice` a duplicate of one in `recent_invoices_same_vendor`?"
    },
    "discrepancy_severity": {
      "type": "score",
      "instructions": "How severe is any discrepancy between `invoice` and `purchase_order`?",
      "criteria": [
        "none: amounts and quantities match",
        "minor: rounding or within tolerance",
        "material: price or quantity differs beyond tolerance",
        "critical: large overcharge, unknown items or likely fraud"
      ]
    },
    "disposition": {
      "type": "choice",
      "instructions": "What should accounts payable do with `invoice`?",
      "criteria": {
        "approve": "matches and can be paid",
        "hold_for_review": "needs a person to resolve a discrepancy",
        "request_credit_note": "overbilled; ask the vendor to correct",
        "reject": "duplicate, fraudulent or not ours"
      }
    },
    "urgency": {
      "type": "score",
      "instructions": "How urgent is handling `invoice`, given its due date and any discount terms?",
      "criteria": [
        "no time pressure",
        "due within weeks",
        "due within days or discount expiring"
      ]
    }
  }
}

What is automated invoice processing?

Automated invoice processing means software reads each supplier invoice and decides what happens next without a clerk handling every one. Laya covers the decision step after extraction: does the invoice match the purchase order, is it a duplicate, how serious is any discrepancy, and should it be approved, held or rejected, each with a probability.

OCR and document AI now pull invoice numbers, line items and totals out of PDFs reliably. What remains manual is the decision layer. For each invoice, an accounts-payable clerk compares it with the purchase order and goods receipt (the three-way match), checks whether the vendor has already billed it, judges whether a price difference is within tolerance, and chooses what happens next: pay, hold, ask for a credit note or reject.

Rules engines handle the clean cases: exact match, pay. They struggle with the rest, because the rest is fuzzy: a unit price 7% above the PO, a line with a slightly different SKU description, an invoice number that differs by one character from last month's. Those exceptions are where clerks spend their time, and where overpayments and duplicate payments slip through.

This decision layer has a fixed shape (the same five questions for every invoice), structured input (JSON from your extraction step and ERP), and asymmetric error costs (paying a duplicate is far worse than holding a good invoice for a day). It is a strong fit for a calibrated decision model with explicit escalation.

Why a fine-tuned decision model rather than an LLM

Laya ships a checkpoint built for this workflow. laya-typed-decisions is ModernBERT-large (421M parameters, 1,024-token context) fine-tuned on four synthetic typed-decision workflows, one of which is invoice_processing. On the model card's typed-decisions benchmark (400 cases, 2,000 decisions), it reaches 0.766 accuracy overall and 0.804 on invoice processing, above a 0.735 teacher self-agreement ceiling and above the published 0.727 for TypeSafe Jev 1.13.0. Its Brier score is 0.062 against Jev's published 0.148.

The context matters: the same benchmark puts the base English checkpoint at 0.362, below the 0.461 per-question majority baseline. The capability comes from fine-tuning. That is the right mental model for invoice decisions generally: a small model specialised on your decision is faster and better calibrated than a general model prompted for it.

Other reasons to prefer a decision model here:

  • No generated text. Answers are one of your labels or a probability. Nothing to parse, no invented invoice numbers or explanations. The answer can still be wrong, which is what the probabilities and thresholds are for.
  • Calibrated probabilities let you set tolerance for automation in terms of expected error, rather than trusting a free-text "looks fine".
  • Latency and cost. Five questions are answered in one forward pass (the model card reports 84.5 ms for five questions on a T4 for the English checkpoint, which uses the same ModernBERT-large encoder; it publishes no separate latency figure for typed-decisions, and network time is extra), billed as 5 credits. See pricing.

Designing invoice questions for the typed-decisions checkpoint

The router recognises the invoice-processing workflow by its exact set of question ids: discrepancy_severity, disposition, duplicate, matches_order and urgency. Use those ids and no others if you want to match the fine-tuned workflow. Adding a sixth question, or renaming one, means the set no longer matches.

Even with matching ids, the hosted router does not switch checkpoints silently. Pass "model": "typed-decisions" in the request body. (The self-hosted Router also supports opt-in auto task detection on the id set, which is off by default.)

Be clear about what is and is not published. The model card and package publish the workflow names and question ids, not the exact instructions and criteria text used in the synthetic training data. The criteria in the example on this page are illustrative, written for a typical three-way-match process. The fine-tuned checkpoint learned from specific synthetic phrasing and synthetic invoices, so your wording and your data will differ. Validate on a labelled sample of your own invoices before automating anything.

Question-by-question:

  • matches_order (noul): the three-way-match check. Put the invoice, PO and goods receipt in the state as structured JSON so the comparison is visible.
  • duplicate (noul): include the vendor's recent invoices (number, date, total) in the state. The model can only compare what it can see. Keep exact-duplicate checks (same number, same total) in your ERP; use Laya for near-duplicates.
  • discrepancy_severity (score): four ordinal levels, each defined by evidence. Put your tolerance rule in the level text if you have one.
  • disposition (choice): the action. Keep an explicit human option (hold_for_review).
  • urgency (score): include due date and discount terms in the state.

Mind the token budget: the checkpoint reads 1,024 tokens per question, about 768 of them for state. Send only the relevant lines, not the full vendor master.

Thresholds and escalation for accounts payable

In accounts payable, the question is not "what is the answer" but "when is it safe not to look". Build the policy around confidence and a conservative default.

Use confidence, not act_probability. The model card and issue #185 report that action.act_probability reads close to 1.0 for almost every input and that its raw logits run against correctness (AUROC 0.30 on 396 labelled decisions), while confidence reached 0.77 on the same items. Log act_probability so you can add it as a second condition once a checkpoint fixes it.

Refit calibration on your data. The typed-decisions checkpoint's ECE on its own benchmark is 0.213, which is higher than its Brier score suggests; the model card reports that refitting one temperature per (question type, option count) cuts ECE sharply on the base checkpoints (0.466 to 0.081 for English). On your invoices, fit temperatures on a few hundred labelled decisions and pick thresholds from the resulting reliability diagram.

A starting policy, deliberately asymmetric:

ConditionAction
duplicate.noul ≥ 0.3Hold; never auto-pay a possible duplicate
disposition = approve, confidence ≥ 0.8, matches_order.noul ≥ 0.9, discrepancy_severity.score < 0.5Auto-approve
disposition = request_credit_note, confidence ≥ 0.7Draft credit-note request for a clerk to send
Anything elseHold for review, prioritised by urgency.score
Show technical details· python sample
python
def ap_policy(a):
    if a["duplicate"]["noul"] >= 0.3:
        return "hold:possible_duplicate"
    d = a["disposition"]
    if (d["choice"] == "approve" and d["confidence"] >= 0.8
            and a["matches_order"]["noul"] >= 0.9
            and a["discrepancy_severity"]["score"] < 0.5):
        return "auto_approve"
    if d["choice"] == "request_credit_note" and d["confidence"] >= 0.7:
        return "draft_credit_note"
    return "hold:review"

Note the noul thresholds use the raw probability (noul), not confidence, because the direction matters. Track the auto-approve rate and audit a random sample of auto-approved invoices every week. See act/escalate routing.

Integration: sending extracted invoices to /v1/systemone

Call the endpoint after your extraction step and ERP lookup, with model set to typed-decisions.

Show technical details· bash sample
bash
curl -s https://api.laya.studio/v1/systemone \
  -H "Authorization: Bearer lsk_live_..." \
  -H "Content-Type: application/json" \
  -d @invoice_request.json   # {"model": "typed-decisions", "state": {...}, "questions": {...}}
Show technical details· python sample
python
import os, requests

def decide_invoice(state: dict, questions: dict) -> dict:
    assert set(questions) == {"discrepancy_severity", "disposition", "duplicate",
                              "matches_order", "urgency"}, "ids must match the workflow"
    r = requests.post(
        "https://api.laya.studio/v1/systemone",
        headers={"Authorization": "Bearer " + os.environ["LAYA_API_KEY"]},
        json={"model": "typed-decisions", "state": state, "questions": questions},
        timeout=10,
    )
    r.raise_for_status()
    body = r.json()
    assert body["routing"]["model"] == "typed-decisions"
    return body["answers"]
Show technical details· typescript sample
typescript
const IDS = ["discrepancy_severity", "disposition", "duplicate", "matches_order", "urgency"];

export async function decideInvoice(state: object, questions: Record<string, unknown>) {
  if (Object.keys(questions).sort().join() !== IDS.join()) throw new Error("question ids must match");
  const res = await fetch("https://api.laya.studio/v1/systemone", {
    method: "POST",
    headers: { Authorization: "Bearer " + process.env.LAYA_API_KEY, "Content-Type": "application/json" },
    body: JSON.stringify({ model: "typed-decisions", state, questions }),
  });
  if (!res.ok) throw new Error("laya " + res.status);
  return (await res.json()).answers;
}

Store the request and full response with the invoice record. Auditors will ask why an invoice was auto-approved, and the stored probabilities are the answer. Sign up for a key or read the docs.

Data stays in Switzerland: supplier invoices are not stored

Invoices carry supplier bank details, prices and contract terms. Laya Studio's primary inference pool runs on dedicated GPUs located in Switzerland, and every API response says where it was processed in the x-laya-region header. The text and questions you send are processed in memory and discarded when the answer is returned: they are never written to a database or log, and never used to train anything.

Turn on Swiss-only mode for a workspace (or send the x-laya-residency: ch header on a request) and requests are only ever answered in Switzerland. If the Swiss pool is unavailable you get an error, never a silent detour abroad. For billing and debugging, only request metadata (time, status, number of questions, latency) is kept, for 30 days. You remain responsible for your legal basis to process personal data. Details: Swiss data residency.

Limitations of automated invoice decisions

  • Synthetic training data. The typed-decisions checkpoint was fine-tuned on synthetic workflows. The 0.804 invoice-processing figure is on that benchmark's test split, not on real invoices. Your accuracy will differ; measure it.
  • The criteria text is yours. The model card does not publish the exact criteria used in training. Wording that differs from the training data may reduce accuracy; test variations.
  • It does not do arithmetic reliably. An encoder classifier reads numbers as tokens. Compute totals, tolerances and exact duplicate matches in code and put the results in the state (for example "price_diff_pct": 6.9), rather than asking the model to calculate.
  • Context is limited to about 768 tokens of state. Invoices with hundreds of lines need summarising first.
  • Score questions are the weakest primitive (0.723 accuracy by primitive on typed-decisions, against 0.857 for noul). Treat discrepancy_severity as a triage signal.
  • Not an approval authority. Keep segregation of duties and payment controls in place; Laya informs the decision, your controls enforce it.

Frequently asked questions

How can I detect duplicate invoices automatically?
Include the extracted invoice and the relevant recent invoices from the same vendor in the state and ask the duplicate question. Laya judges near-duplicates, such as an invoice number that differs by one character, and returns a probability; hold anything above a low threshold for a clerk.
Are invoices stored when processed by Laya Studio?
No. Invoice content is processed in memory on GPUs in Switzerland and discarded once the answer is returned. Only request metadata (time, status, number of questions, latency) is kept, for 30 days.
Which question ids select the invoice-processing workflow?
Exactly discrepancy_severity, disposition, duplicate, matches_order and urgency. Also pass "model": "typed-decisions" so the fine-tuned checkpoint answers.
How accurate is Laya on invoice processing?
The model card reports 0.804 accuracy for the typed-decisions checkpoint on the invoice-processing workflow of its synthetic benchmark. Accuracy on your own invoices must be measured on a labelled sample.
Can Laya extract fields from invoice PDFs?
No. Laya makes decisions over text or JSON you already have. Use an OCR or document-extraction step first and pass the structured result as the state.
Should I let Laya auto-approve invoices?
Only above thresholds you have validated on labelled invoices, with duplicates always held and a regular audit sample of auto-approved items.
Can I add my own questions to the invoice workflow?
Yes, in a separate request. Adding them to the same request changes the id set, so it no longer matches the workflow the checkpoint was fine-tuned on.

Sources

Last updated . Laya Studio is an independent hosted service for the open-source Laya model (Apache-2.0, © Convai Innovations) and is not affiliated with Convai Innovations or TypeSafe.