Limai Docs
API ReferenceWebhooks

Document Validated Event

Webhook event fired when a document's validations settle.

The DOCUMENT_VALIDATED event fires when a document's validations settle -- that is, when nothing is still pending or running. It reports the aggregate validation outcome for the file, a per-rule summary and a per-script summary.

It covers both kinds of check: the model's validation rules and its custom validations. A document validated by rules alone gets the event too, published as soon as its rules are evaluated -- during extraction, or by a revalidate call that dispatched nothing.

When It Triggers

The event fires each time the file's validation state settles: the model's rules have been evaluated for the current extraction and no custom validation for that file is PENDING or RUNNING. A model with neither rules nor custom validations never settles, and the event never fires for its documents. Neither does a document extracted before its model had any rule, until it is extracted again or revalidated.

Custom validations run on two independent paths -- the automatic (static) batch that runs during processing, and validations an agent submits afterward. Because these paths are independent and whether an agent will submit is not knowable in advance, the event can fire more than once for the same file:

  • The rules and the static batch settle and the event fires with the current outcome.
  • If an agent later submits validations, the file settles again and the event fires a second time.

Each settle publishes exactly one event covering everything the document has: rules and scripts are never reported as two separate events.

Delivery is at-least-once: a retried or re-delivered event repeats an eventId you have already seen, so deduplicate on it.

When it fires more than once, the event with the later timestamp supersedes the earlier one. Treat the payload as "the current validation state of this file", keyed by fileId. Delivery is queued, retried and processed concurrently, so receipt order is not state order -- compare timestamp before overwriting what you already stored, or re-read the document with GET /v1/deployments/{deploymentId}/documents/{fileId} instead of applying the event blind. This also holds across the lifecycle events: a later DOCUMENT_VALIDATED supersedes an earlier DOCUMENT_VALIDATION_FAILED.

Unlike the lifecycle events, this one is document-scoped, not attempt-scoped. data.validationAttemptId is the newest attempt that contributed to the document, while each entry in data.results carries the attempt it came from -- after a partial revalidation those differ. Rules start no attempt, so data.validationAttemptId is null for a document whose only validations are rules.

This is a document event, routed to subscriptions by deployment route.

Payload Structure

Response200
{
  "eventId": "evt_validated_654",
  "eventType": "DOCUMENT_VALIDATED",
  "timestamp": "2024-01-15T10:40:00Z",
  "organizationId": "org_xyz789",
  "projectId": "proj_abc123",
  "deploymentId": "dep_123abc",
  "viaDeploymentId": "dpl_prod01",
  "deploymentName": "production",
  "schemaVersionId": "sv_7",
  "extractionSchemaId": "schema_456def",
  "fileId": "file_789ghi",
  "data": {
    "status": "failed",
    "failCount": 3,
    "ruleFailCount": 1,
    "validationAttemptId": "att_2b",
    "results": [
      {
        "scriptKey": "totals-check",
        "source": "STATIC",
        "status": "PASSED",
        "validationAttemptId": "att_1a"
      },
      {
        "scriptKey": "agent-validator",
        "source": "AGENT",
        "status": "FAILED",
        "validationAttemptId": "att_2b"
      }
    ],
    "rules": [
      {
        "check": "vrule_2a8",
        "name": "Invoice Number is not empty",
        "scope": "FIELD",
        "operator": "IS_NOT_EMPTY",
        "passed": true
      },
      {
        "check": "vrule_9k1",
        "name": "Net Amount vs Gross Amount",
        "scope": "CROSS_FIELD",
        "operator": "EQUALS",
        "passed": false
      }
    ]
  }
}

A document validated by rules alone carries the same shape with an empty results array and no attempt id:

Response200
{
  "eventId": "evt_validated_655",
  "eventType": "DOCUMENT_VALIDATED",
  "timestamp": "2024-01-15T10:41:00Z",
  "organizationId": "org_xyz789",
  "projectId": "proj_abc123",
  "deploymentId": "dep_123abc",
  "viaDeploymentId": null,
  "deploymentName": null,
  "schemaVersionId": null,
  "extractionSchemaId": "schema_456def",
  "fileId": "file_789ghj",
  "data": {
    "status": "passed",
    "failCount": 0,
    "ruleFailCount": 0,
    "validationAttemptId": null,
    "results": [],
    "rules": [
      {
        "check": "vrule_2a8",
        "name": "Invoice Number is not empty",
        "scope": "FIELD",
        "operator": "IS_NOT_EMPTY",
        "passed": true
      }
    ]
  }
}

Payload Fields

FieldTypeDescription
eventIdstringUnique event identifier. Use for idempotency.
eventTypestringAlways "DOCUMENT_VALIDATED"
timestampISO 8601When the validation state settled (UTC)
organizationIdstringYour organization identifier
projectIdstringThe project the document belongs to
deploymentIdstringThe ID of the model that processed this document
viaDeploymentIdstring | nullThe ID of the deployment the request arrived through. null when the request named the model directly
deploymentNamestring | nullThe name of that deployment, such as "production". null whenever viaDeploymentId is null
schemaVersionIdstring | nullThe ID of the frozen schema version the run resolved to. null when the deployment tracks the draft schema
extractionSchemaIdstringThe extraction schema ID of that model
fileIdstringThe validated file identifier
data.status"passed" | "failed" | "error"Aggregate outcome over rules and custom validations. failed takes precedence over error, which takes precedence over passed.
data.failCountnumberThe file's validation fail count after recompute, failing rules included
data.ruleFailCountnumberHow many of those failures come from validation rules. 0 for a document with no rule failures
data.validationAttemptIdstring | nullThe newest attempt id on the file. Correlates with the DOCUMENT_VALIDATION_STARTED and DOCUMENT_VALIDATION_FAILED events of that attempt. null when the document's only validations are rules, and for attempts started before attempt ids existed.
data.resultsarrayPer-script summary (see below). Custom validations only -- rules are never listed here
data.rulesarrayPer-rule summary: one {check, name, scope, operator, passed} entry per rule with a result on the document, pass or fail, every scope. Empty when the model has no rules
data.results[].scriptKeystringIdentifier of the validation script
data.results[].source"STATIC" | "AGENT"Whether the validation came from the automatic batch or an agent submission
data.results[].statusstringPer-script status, e.g. "PASSED", "FAILED", "RESEND_PASSED", "RESEND_FAILED", "ERROR"
data.results[].validationAttemptIdstring | nullThe attempt this individual result belongs to. null for results written before attempt ids existed.

Attempt Ids Can Differ Within One Payload

data.validationAttemptId is the newest attempt id on the file, not a guarantee that every result belongs to it. A partial re-validation -- {"include": ["AGENT"]} or {"include": ["STATIC"]} -- starts a new attempt for one lane only and leaves the other lane's rows on their previous attempt. The settled payload then mixes attempts: the re-run lane carries the new id, the untouched lane carries the older one.

Compare data.results[].validationAttemptId against data.validationAttemptId when you need to know which results are fresh. Results whose attempt id is older were carried over from an earlier attempt and are still the current state of that script -- they are not stale in the sense of being wrong, only in the sense of not having been re-run.

Full Findings Are Not in the Payload

The payload carries a summary per script and per rule, not the full validation findings. Detailed findings are intentionally excluded for payload size, retry, and signing reasons. A rule entry says which rule and whether it held; which cells or rows it failed on, and with what expected and actual values, is not in the payload.

To get the complete results, read the document with the fileId from the payload: Document Data with include=validations for one flat validations.rules[] list with per-failure detail, or get-file-data for the nested shape.

Rule Evaluation Failures

When rule evaluation fails for a document, DOCUMENT_VALIDATION_FAILED is emitted and the document still settles: this event arrives with data.status: "error", an empty data.rules array and data.ruleFailCount: 0. A document is never left waiting on a validation that cannot complete.

Handling the Event

function handleDocumentValidated(event) {
const { eventId, fileId, data } = event

console.log(`Document ${fileId} validation settled: ${data.status} (failCount: ${data.failCount})`)

if (data.status === "passed") {
  fetchFileData(fileId).then(exportToDownstreamSystems)
} else {
  const failedRules = (data.rules ?? []).filter(r => !r.passed)
  const failedScripts = data.results.filter(r => r.status.includes("FAILED"))
  routeToReview(fileId, [...failedRules, ...failedScripts])
}
}

On this page