Classify endpoint
The classify endpoint scores a document against a list of labels you supply and returns a ranked list of predictions:
POST https://api.nutrient.io/extraction/classifyUse the classify endpoint to decide where a document goes before you extract anything from it. A claim form, a medical report, and a cover letter are all “a PDF” until something reads them. Classify makes that call so an intake queue can route each file to the right destination. To pull fields out of a document once it’s routed, refer to the extract endpoint guide.
How classification works
Classification is zero-shot. The API scores the document against the labels in your request, not labels learned from a training set. There’s no model to train and no template to maintain. Switching to a new set of document types means sending a new label list.
Each document is scored two ways: once from its extracted text, and once from its page images. The two scores are merged into a single ranked list. Dedicated scoring models do the work, not a generative large language model (LLM) or vision language model (VLM).
Request formats
Every classify request needs at least two labels. Each label has a label name and an optional description. The description matters: The classifier scores against it as much as against the name, so a specific description gives a sharper boundary than a vague one.
Multipart form upload
Send the document as the file form field, and send the labels inside a JSON-serialized instructions field:
curl -X POST https://api.nutrient.io/extraction/classify \ -H "Authorization: Bearer your_api_key_goes_here" \ -F "file=@document.pdf" \ -F 'instructions={"labels":[{"label":"invoice","description":"A commercial invoice or bill."},{"label":"contract","description":"A legal agreement between parties."},{"label":"resume","description":"A résumé or CV from a job applicant."}]}'import json
import requests
labels = [ {"label": "invoice", "description": "A commercial invoice or bill."}, {"label": "contract", "description": "A legal agreement between parties."}, {"label": "resume", "description": "A résumé or CV from a job applicant."},]
with open("document.pdf", "rb") as file: response = requests.post( "https://api.nutrient.io/extraction/classify", headers={"Authorization": "Bearer your_api_key_goes_here"}, files={"file": file}, data={"instructions": json.dumps({"labels": labels})}, )
result = response.json()if result["status"] != 200: raise RuntimeError(result["errorMessage"])
print(result["output"]["classification"])import fs from "node:fs";
const labels = [ { label: "invoice", description: "A commercial invoice or bill." }, { label: "contract", description: "A legal agreement between parties." }, { label: "resume", description: "A résumé or CV from a job applicant." },];
const form = new FormData();form.append("file", new Blob([fs.readFileSync("document.pdf")]), "document.pdf");form.append("instructions", JSON.stringify({ labels }));
const response = await fetch("https://api.nutrient.io/extraction/classify", { method: "POST", headers: { Authorization: "Bearer your_api_key_goes_here" }, body: form,});
const result = await response.json();if (result.status !== 200) { throw new Error(result.errorMessage);}
console.log(result.output.classification);JSON URL input
To classify a document that’s already reachable at a URL, send a JSON body with url and labels as top-level properties:
curl -X POST https://api.nutrient.io/extraction/classify \ -H "Authorization: Bearer your_api_key_goes_here" \ -H "Content-Type: application/json" \ -d '{ "url": "https://storage.example.com/contract.pdf", "labels": [ { "label": "invoice", "description": "A commercial invoice or bill." }, { "label": "contract", "description": "A legal agreement between parties." } ] }'Request options
All options go alongside labels, inside the multipart instructions JSON or the top level of a JSON body.
| Option | Type | Description |
|---|---|---|
labels | array | Required. At least two candidate labels. Each item has a label string and an optional description. |
topK | integer | Maximum number of ranked predictions to return. 0 or omitted returns all of them. |
textWeight | number | Set to 0 to skip the text branch and its text extraction. Any other value between 0 and 1 is accepted but doesn’t change the result; the engine balances the two branches per document. |
imageWeight | number | Set to 0 to skip the image branch and its page rendering. Any other value between 0 and 1 is accepted but doesn’t change the result; the engine balances the two branches per document. |
Leave at least one branch enabled. With both weights set to 0, no branch produces a score.
Response
A successful request returns HTTP 200 with the top label, its score, and the full ranked list:
{ "status": 200, "requestId": "req_cl_001", "output": { "classification": { "label": "invoice", "score": 0.92, "predictions": [ { "label": "invoice", "score": 0.92 }, { "label": "contract", "score": 0.14 }, { "label": "resume", "score": 0.03 } ] } }, "metrics": { "processingTimeMs": 1840, "pagesProcessed": 1 }, "usage": { "data_extraction_credits": { "cost": 1, "remainingCredits": 4999 } }}| Field | Description |
|---|---|
output.classification.label | The top-ranked label. |
output.classification.score | The top label’s score. Present when the engine returns one. |
output.classification.predictions | Every candidate label with its score, highest first. topK caps the length of this list. |
metrics.pagesProcessed | The number of pages scored. This is the number of credits the request cost. |
Reading the scores
Each score is an independent confidence for that label, in the range 0 to 1. The scores aren’t a probability distribution and don’t sum to 1. A cover letter attached to a contract can legitimately score high on both correspondence and contract.
That independence is what lets a router flag an ambiguous document instead of forcing it into one bucket. Compare the top score with the runner-up, and send close calls to manual review:
def route(classification, routes, ambiguity_gap=0.15): predictions = classification["predictions"] top = predictions[0] runner_up = predictions[1] if len(predictions) > 1 else None
if runner_up and top["score"] - runner_up["score"] < ambiguity_gap: return "queue:manual-review"
return routes.get(top["label"], "queue:manual-review")The threshold is yours to choose. Because the label list and the routes map are both parameters, the same function serves insurance intake, legal filings, or HR onboarding paperwork. For a complete worked example, refer to the document routing walkthrough.
Stored classifiers
You can save a label list as a classifier in the Data Extraction Studio(opens in a new tab) and run it by reference. Send processor with the classifier’s ID instead of labels, and optionally pin a published version. Add storeRun: true to keep the run in your processing history. Refer to the API reference(opens in a new tab) for the full request schema.
Pricing
Classify costs a flat 1 credit per page, independent of parse mode. A 10-page document costs 10 credits. Refer to the pricing guide for plan thresholds.
Supported file types
The API reference(opens in a new tab) lists four input types for classify: application/pdf, image/png, image/jpeg, and image/tiff. Office documents, which the parse and extract endpoints accept, aren’t listed for classify. Refer to the supported file types guide for the extensions and MIME types behind each format.
Errors
A request with fewer than two labels returns HTTP 400 with errorDetails.failingPaths pointing at $.labels. Failed requests always return a JSON body with a non-200 status and an errorMessage. Refer to the error handling guide for the full status code reference.
Try it
Open the Classify workbench in Studio(opens in a new tab) to run a document against your own labels before wiring the endpoint into a pipeline.