Limai Docs
API ReferenceWebhooks

Document Classified Event

Webhook event fired when a classifier reaches a decision on a document.

The DOCUMENT_CLASSIFIED event fires when a classifier reaches a decision on a document. It carries both outcomes: the document routed to a deployment, or it did not.

There is no separate "unclassified" event. Read data.status to tell the two apart. This is deliberate -- a consumer that subscribes to classification results should never silently miss the documents that failed to route.

When It Triggers

  • Routed -- the classifier picked a deployment and the document moved into it. data.status is "classified" and routedDeploymentId names the deployment.
  • Not routed -- the classifier looked at the document and declined to route it. data.status is "unclassified", routedDeploymentId is null, and data.unclassifiedReason says why.
  • Routed by hand -- someone routed the document manually from the classifier inbox. data.source is "MANUAL" and data.manualRoutedBy is the user who did it.

This is a classifier event, routed to subscriptions by classifier route, not by deployment. That matters: a document that did not route has no deployment to key off.

Payload Structure

Response200
{
  "eventId": "evt_classified_101",
  "eventType": "DOCUMENT_CLASSIFIED",
  "timestamp": "2024-01-15T10:31:05Z",
  "organizationId": "org_xyz789",
  "projectId": "proj_abc123",
  "classifierId": "clf_123abc",
  "classificationId": "cls_456def",
  "fileId": "file_789ghi",
  "routedDeploymentId": "dep_123abc",
  "extractionSchemaId": "schema_456def",
  "data": {
    "status": "classified",
    "confidenceScore": 0.94,
    "decisionConfidence": 0.91,
    "reasoning": "Header carries a VAT number and an invoice total",
    "unclassifiedReason": null,
    "consideredDeploymentId": null,
    "source": "AUTOMATIC",
    "manualRoutedBy": null
  }
}

An unclassified outcome on the same event:

Response200
{
  "eventId": "evt_classified_102",
  "eventType": "DOCUMENT_CLASSIFIED",
  "timestamp": "2024-01-15T10:32:11Z",
  "organizationId": "org_xyz789",
  "projectId": "proj_abc123",
  "classifierId": "clf_123abc",
  "classificationId": "cls_789jkl",
  "fileId": "file_012mno",
  "routedDeploymentId": null,
  "extractionSchemaId": null,
  "data": {
    "status": "unclassified",
    "confidenceScore": 0.22,
    "decisionConfidence": 0.88,
    "reasoning": "Closest match was the delivery note schema, but no line items were present",
    "unclassifiedReason": "Confidence below the routing bar",
    "consideredDeploymentId": "dep_999zzz",
    "source": "AUTOMATIC",
    "manualRoutedBy": null
  }
}

Payload Fields

FieldTypeDescription
eventIdstringUnique event identifier. Use for idempotency.
eventTypestringAlways "DOCUMENT_CLASSIFIED"
timestampISO 8601When the decision was reached (UTC)
organizationIdstringYour organization identifier
projectIdstringThe project the classifier belongs to. See the note below for global classifiers.
classifierIdstringThe classifier that made the decision
classificationIdstringThis classification run
fileIdstringThe classified file. Stable across routing -- the same id before and after the document moves into a deployment.
routedDeploymentIdstring | nullThe deployment the document routed into, or null when it did not route
extractionSchemaIdstring | nullThe extraction schema of that deployment, or null when it did not route
data.status"classified" | "unclassified"The outcome
data.confidenceScorenumber | nullHow confident the classifier is in the match, 0--1
data.decisionConfidencenumber | nullHow confident the classifier is in the decision itself, including a decision not to route
data.reasoningstring | nullThe classifier's own explanation
data.unclassifiedReasonstring | nullWhy the document did not route. null when it did.
data.consideredDeploymentIdstring | nullA deployment the classifier weighed and rejected. Never treat this as a routing target.
data.source"AUTOMATIC" | "MANUAL"Whether the classifier or a person made the call
data.manualRoutedBystring | nullThe user who routed it by hand, when source is "MANUAL"

consideredDeploymentId is not a route

When a document does not route, the classifier may still have had a leading candidate. That candidate appears as data.consideredDeploymentId, never as routedDeploymentId. Treating it as a route would send the document somewhere the classifier explicitly declined to send it.

projectId for a global classifier

A global classifier is shared across every project in your organization, so a single classification can be delivered to subscriptions in several projects at once. When the document routed, projectId is the routed deployment's project and every subscriber sees the same value. When it did not route there is no such project, so each subscription receives its own projectId — the project the webhook is configured in. classifierId, classificationId and fileId are the same for every subscriber and are the safest keys for your own bookkeeping.

Handling the Event

function handleDocumentClassified(event) {
const { fileId, routedDeploymentId, data } = event

if (data.status === "classified") {
  console.log(`${fileId} routed to ${routedDeploymentId} (${data.confidenceScore})`)
  return
}

console.warn(`${fileId} not routed: ${data.unclassifiedReason}`)
routeToManualTriage(fileId, data.reasoning)
}

Notes

An unclassified outcome is not an error. If you retry on it, you will re-run the same classifier over the same document and get the same answer. Use DOCUMENT_CLASSIFICATION_FAILED to catch the runs that genuinely broke.

On this page