POST /v1/classify
Reference for POST /v1/classify: identify a document's type for 0.2 credits, with candidates and a type verdict, to route it before analysing it.
POST https://api.constaia.com/v1/classify answers a single question: which document is this. It does not extract fields or run checks, and it costs 0.2 credits per document, whatever its page count (0 in test mode).
Use it when you receive documents without knowing what they are (a mailbox, a shared folder, a form with a single "attach your documents" field) and need to route them: decide which queue each one goes to, or which expect and checks to apply next in POST /v1/analyze. If you already know which document you expect, call analyze with expect directly: you save one call.
Request
It accepts exactly the same inputs as POST /v1/analyze: multipart/form-data with file and options, or JSON with file_url or file_base64 + filename. Same formats, same 20 MB limit and the same -F expect=… shortcut.
Options are validated with the same strict schema. Those that have an effect on a classification are:
| Option | Description |
|---|---|
expect | Accepted type or types. With expect you get a type verdict. |
language | Language of document.label and of the verdict.reasons messages. |
metadata | Your data (up to 20 keys), returned as sent. |
async | true to answer 202 immediately and receive the result by webhook. |
curl https://api.constaia.com/v1/classify \
-H "Authorization: Bearer $CONSTAIA_API_KEY" \
-F file=@dni_valid.jpg \
-F 'options={"expect":["es_dni","es_nie","passport"],"language":"en"}'Response: the classification object
{
"id": "an_01J9Z8Q3K4M5N6P7Q8R9S0T1V2",
"object": "classification",
"status": "completed",
"livemode": false,
"created_at": "2026-09-29T10:00:00Z",
"completed_at": "2026-09-29T10:00:01Z",
"file": { "name": "dni_valid.jpg", "mime_type": "image/jpeg", "pages": 1, "size_bytes": 482133 },
"document": { "type": "es_dni", "label": "Spanish ID card (DNI)", "confidence": 0.97 },
"candidates": [{ "type": "es_dni", "confidence": 0.97 }],
"verdict": {
"expected": ["es_dni", "es_nie", "passport"],
"match": true,
"status": "valid",
"reasons": [{ "code": "type_match", "severity": "info", "message": "The document is Spanish ID card (DNI)." }]
},
"warnings": [],
"usage": { "credits": 0, "pages": 1 },
"metadata": {}
}| Field | Description |
|---|---|
id | Id with prefix an_. |
object | Always "classification". |
status | queued, processing, completed or failed. |
livemode | true with a live key. |
created_at, completed_at | ISO 8601 dates; completed_at is null until it finishes. |
file | name, mime_type, pages, size_bytes. |
document | Most likely type: type, label, confidence (0–1). null if it could not be classified. |
candidates | Candidate types with their confidence. Useful to decide yourself when confidence is low. |
verdict | Only with expect. It is a type verdict: type_match (the type is in expected), type_mismatch (another catalogue type is recognised, invalid) or type_unknown (it could not be identified or confidence is below 0.5, review). It may add low_quality or low_confidence as warning, which lead to review. |
warnings | Quality warnings detected. |
usage | credits (0.2 in live, 0 in test) and pages. |
metadata | Your metadata. |
error | Only when status is failed. |
A classify verdict of valid only says the type is the expected one. It does not check expiry, holder or any other rule: that is what analyze is for.
Like analyze, it can answer 202 if it goes asynchronous; in that case the result arrives in the analysis.completed webhook with data.object: "classification". Errors are the same as in analyze.
Pattern: classify, then analyse
Classify the document with the list of types your flow knows how to handle.
Route on document.type: each type has its own checks. If the verdict is not valid (type_mismatch, type_unknown or low confidence), send it to manual review.
Analyse with expect set to the detected type and that type's checks.
import { Constaia, type Checks } from "@constaia/sdk";
import { fromPath } from "@constaia/sdk/node";
const constaia = new Constaia();
const checksByType: Record<string, Checks> = {
payment_receipt: { expectedAmount: 45, expectedReference: "INSCRIPCION 123" },
medical_certificate_sport: { maxAgeDays: 365, requireSignature: true },
invoice: {},
};
const file = await fromPath("./attachment.pdf");
const classification = await constaia.classify(file, { expect: Object.keys(checksByType) });
const type = classification.document?.type;
if (classification.verdict?.status !== "valid" || !type) {
console.log("Send to manual review:", classification.candidates);
} else {
const analysis = await constaia.analyze(file, { expect: type, checks: checksByType[type] });
console.log(type, analysis.verdict?.status);
}With this pattern you pay an extra 0.2 credits per document. If your documents are always of a few types, it is usually cheaper to call analyze directly with expect as a list and branch on document.type, as explained in Document types.
Next steps
POST /v1/analyze
Full reference for POST /v1/analyze: request formats, every option and check, the analysis object field by field, status codes and examples.
Stored analyses
Retrieve, list with filters and cursor pagination, export and delete analyses with /v1/analyses, and download exports through signed /v1/files URLs.