Limai Docs
API Reference

Validations

How validation rules and custom validations appear in API responses and webhooks

A document is checked two ways, and both reach you through the same fields.

Validation rules are declarative checks configured on the model — an amount that must be positive, a date that must not be empty, a line-item total that must match the invoice total. They evaluate as part of extraction and cost nothing.

Custom validations are the code you supply: validation scripts that run in a sandbox after extraction, and validator agents that submit a verdict. They are dispatched as a validation attempt, are billed per dispatch, and can re-derive a value when a check is configured to RESEND.

Validation rulesCustom validations
Identified bythe rule's idthe script's key
Cell entry sourceRULESCRIPT or AGENT
Document-level listvalidations.rulesvalidations.checks
ScopesFIELD, COLUMN, CROSS_FIELD, ROW, DOCUMENTthe whole document
Attempt lifecyclenone — evaluation is synchronousDOCUMENT_VALIDATION_STARTED, then a settle
Costfreebilled per dispatch

Both contribute to the document's single validation status and to validationFailCount.

Rule results

Every rule result — on a cell, in a document-level list, or in a webhook payload — carries the same identity fields:

FieldTypeDescription
checkstringThe rule's id. A cell entry on the nested endpoints calls it checkKey, and a nested rule group calls it ruleId.
namestringThe rule's name, or a generated label when it has none. Never empty.
scopeFIELD | COLUMN | CROSS_FIELD | ROW | DOCUMENT | nullWhere the rule applies. null when the rule no longer exists.
operatorstring | nullIS_EMPTY, IS_NOT_EMPTY, EQUALS, DOES_NOT_EQUAL, CONTAINS, DOES_NOT_CONTAIN, STARTS_WITH, ENDS_WITH, IS_GREATER_THAN, IS_LESS_THAN or IS_BETWEEN.
passedbooleanWhether the rule held.
expectedstring | nullWhat the rule required, rendered for reading: > 0, non-empty value, contains "PO-", between 1 and 99.
actualstring | nullThe value the rule saw.
messagestring | nullHuman-readable failure detail. null on a passing result.

How name is built

A rule's own name is used when it has one. Otherwise the label is derived from the rule's operator, its configuration and the names of the columns it references:

RuleGenerated name
Field rule, amount greater than zeroTotal Amount > 0
Field rule, date must be filledInvoice Date is not empty
Cross-field ruleNet Amount vs Gross Amount
Row rule comparing computed to extractedQuantity * Unit Price = Line Total
Document rule over a child tableSUM(Amount)

Labels use column display names, not slugs, so they read the same way they do in the app.

How message is built

message is null while a rule passes. A failing rule reports <name>: expected <expected>, got <actual>:

Total Amount > 0: expected > 0, got -40

When only one side is known the message carries that side — <name>: expected <expected> or <name>: got <actual> — and <name>: failed when neither is. The message is composed when the response is serialized. It is not stored, and it is not configurable per rule; to change what it says, rename the rule.

Rules that no longer exist

A result whose rule was deleted since it was evaluated is still returned. scope and operator are null, and name falls back to a label derived from the stored result: the operator's own label (Is greater than) for field and column results, and Validation for cross-field, row and document results, which store no operator.

Rule ids

check is the rule's id, and it survives edits to the rule's name, operator and configuration. Changing a rule's type in the app replaces the rule: the old id stops appearing and a new one takes its place.

Rules are not versioned. Evaluation reads the rules as they are defined at that moment, so a document keeps the verdict of its last evaluation until it is extracted again or revalidated.

The document's validation status

One status answers "is this document validated?" for both kinds of validation. It is validations.status on the data endpoints, validationStatus on Document Detail, and data.status on DOCUMENT_VALIDATED.

ValueMeaning
failedA rule failed, or a custom check reported findings.
pendingA custom check is queued or running, or an extraction job that has just completed is still evaluating the model's rules. See when the rules lane reports pending.
errorA custom check could not run, or rule evaluation failed for the document.
passedEverything recorded for the document passed.
nullNothing is recorded for the document: no rule verdict and no custom validation. A model with neither kind of check, or a document extracted before its model had any rule.

Precedence is failed > pending > error > passed: a document with one failing rule and one still-running script reports failed.

DOCUMENT_VALIDATED only ever carries a settled status, so pending and null never appear in a webhook payload.

When rules are evaluated

  • During extraction. On a model with at least one rule, evaluation finishes before DOCUMENT_EXTRACTED is published, so a read triggered by that event sees the document's final rule results. A model with no rules has nothing to evaluate, and the event is published as soon as the extraction completes.
  • The previous run's rule results are cleared as the extraction job completes, so a re-extraction never shows the earlier run's verdict. Between that moment and the end of evaluation — the same job, moments later — the status is pending.
  • Documents routed to the extraction agent — Excel files, and models configured for agent or dynamic-mapping extraction — hold to the same ordering: the agent's run evaluates the model's rules before DOCUMENT_EXTRACTED is published, and the document settles afterwards. These documents keep the previous run's rule results until the new evaluation replaces them, so the rules lane never reports pending for them.
  • The synchronous Process File endpoint evaluates rules before it responds, so its response already carries them.
  • POST …/revalidate re-evaluates every rule on every call, free of charge, on top of the custom lanes it was asked for.

Rule evaluation is synchronous and has no validation attempt. No DOCUMENT_VALIDATION_STARTED fires for rules, and validationAttemptId is null for a document whose only validations are rules.

When evaluation fails for a document, DOCUMENT_EXTRACTED is published first and DOCUMENT_VALIDATION_FAILED follows it. The document's status is error from the moment DOCUMENT_EXTRACTED is published, and its rule results are empty.

When the rules lane reports pending

The rules lane reports pending only inside the window above, and only on a model that has rules. Three states that look similar are not pending:

  • A model with no validation rules. Its documents never take pending from the rules lane; their status is the custom aggregate alone.
  • A document extracted before its model had any rule. It holds no rule verdict at all: its status comes from its custom validations, and is null when it has none. Revalidate it to evaluate the current rules against it.
  • A document whose rule evaluation failed. Its status is error, and its rule results are empty.

The window is bounded. A document that has been pending longer than 15 minutes — the validation watchdog's check timeout — reads as error on every surface, so nothing stays pending indefinitely.

pending from a custom validation means something different: a validation script or a validator agent run is still in flight.

Where results appear

EndpointPer-cell entriesDocument-level rule results
Document Data (Flat) and its bulk formvalidations[] on each cellvalidations.rules[] — one entry per rule, every scope, each with its failures[]
Get File Data, Get Files Data, Process Filevalidations[] on each cellvalidations.rules.{crossField,row,document}
Document Detail—validations.{crossField,row,document}, re-evaluated live
DOCUMENT_VALIDATED—data.rules[], one pass/fail entry per rule

Field and column rule results live on the cell they were evaluated against. Cross-field, row and document results are document-level. The flat rules[] list folds all five scopes into one list, so it is the single place to read every rule outcome for a document:

Response200
{
  "validations": {
    "status": "failed",
    "failCount": 2,
    "checks": [],
    "rules": [
      {
        "check": "vrule_7h2",
        "name": "Total Amount > 0",
        "scope": "FIELD",
        "operator": "IS_GREATER_THAN",
        "passed": false,
        "message": "Total Amount > 0: expected > 0, got -40",
        "failures": [
          {
            "table": "invoices",
            "rowId": "row_h1",
            "column": "total_amount",
            "expected": "> 0",
            "actual": "-40"
          }
        ]
      },
      {
        "check": "vrule_9k1",
        "name": "Net Amount vs Gross Amount",
        "scope": "CROSS_FIELD",
        "operator": "EQUALS",
        "passed": false,
        "message": "Net Amount vs Gross Amount: expected 1250.00, got 1180.00",
        "expected": "1250.00",
        "actual": "1180.00",
        "failures": []
      }
    ]
  }
}

expected and actual sit on the entry itself for cross-field and document rules, which produce one result per document. Field, column and row rules produce one result per cell or row, so their evidence is in failures[] instead, and passing rules have an empty failures[].

On this page