> For the complete documentation index, see [llms.txt](https://docs.uptiq.ai/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.uptiq.ai/document-ai/cookbooks/classify-then-extract.md).

# Classify, then extract

**Goal.** Take a document whose type you do not know, work out what it is, and route it to the right extraction schema.

Extraction is schema-driven — naming the wrong `documentType` produces thin or empty results rather than an error. When files arrive from a channel you do not control, classification is the step that makes extraction reliable.

### Generated endpoint reference

Use [Synchronous classification](/document-ai/developer-documentation/classification-1/synchronous-document-classification.md), [asynchronous classification](/document-ai/developer-documentation/classification-1/classify-document-type-async.md), and [classification details](/document-ai/developer-documentation/classification-1/get-classification-details.md) for schemas. Use [asynchronous extraction](/document-ai/developer-documentation/extraction-1/extract-document-data-async.md) for the final submission.

### Before you start

* An API key, exported as `$DOCAI_API_KEY`.
* A document of unknown type.

### Step 1 — Classify

```bash
python -c "
import base64, json
content = base64.b64encode(open('unknown.pdf','rb').read()).decode()
json.dump({'content': content}, open('classify.json','w'))
"

curl -X POST "https://api-docai-uat.uptiq.ai/classify/sync" \
  -H "X-Api-Key: $DOCAI_API_KEY" \
  -H "Content-Type: application/json" \
  --data-binary @classify.json
```

Classification is much faster than extraction — **13 seconds** in our testing, against 93 for a comparable extraction — so the synchronous endpoint is reasonable here in a way it is not for extraction.

```json
{
  "_id": "5b6e2a17-9c04-4d38-8f21-3ae7b0c95d64",
  "documentStatus": "Processed",
  "additionalClassifications": [],
  "status": "success",
  "result": {
    "type": "BalanceSheet",
    "category": "Financial Statement",
    "confidence": 1.0,
    "documentBasis": "Actual",
    "pages": [1],
    "summary": "The document is a standalone balance sheet for Northwind Trading Co. as of December 31, 2025, detailing assets, liabilities, and equity.",
    "documentQuality": { "level": "high", "score": 1.0, "stage": "classification" }
  }
}
```

`result.type` is the value you feed to extraction. `result.summary` is a genuinely useful by-product — a natural-language description you can log, show a reviewer, or use to explain a routing decision.

### Step 2 — Gate on confidence

`result.confidence` runs 0 to 1. Do not route on the type alone.

```bash
TYPE=$(jq -r '.result.type' classification.json)
CONF=$(jq -r '.result.confidence' classification.json)

if (( $(echo "$CONF < 0.8" | bc -l) )); then
  echo "Low confidence ($CONF) for $TYPE — queue for human review"
  exit 0
fi
```

Also check `additionalClassifications`. When it is non-empty the classifier saw more than one plausible answer, which is worth treating as a review signal even if the top confidence looks acceptable.

{% hint style="warning" %}
Pick a threshold from your own tolerance for a wrong extraction, not from the example above. A misrouted document does not error — it silently produces a poor result, which is harder to notice than a failure.
{% endhint %}

### Asynchronous classification, if you prefer

`POST /classify` queues the job instead of waiting. Its response uses **a different wrapper again** — `requestInfo`, not `data`:

```json
{
  "status": "success",
  "message": "Document queued for classification",
  "requestInfo": {
    "_id": "12c9c8db-691e-4a8d-8b43-9f4a4f1fe425",
    "requestId": "55e978e9-6403-4d81-a9b8-25caf8e3430c",
    "storage": {
      "bucket": "docai-uat-bucket-773f5746",
      "key": "documents/classification/a1a6ac48-….pdf"
    }
  }
}
```

Retrieve it with `GET /classifications/{id}`, which wraps in **`classification`**:

```json
{
  "classification": {
    "_id": "12c9c8db-691e-4a8d-8b43-9f4a4f1fe425",
    "status": "Processed",
    "model": "gemini-3",
    "creditReservation": { "cost": 0.03, "pages": 1, "operation": "classification" },
    "result": { }
  },
  "status": "success"
}
```

{% hint style="warning" %}
That is four different wrapper keys across the API — `data`, `extraction`, `requestInfo` and `classification` — plus none at all on the synchronous endpoints. [Conventions](/document-ai/developer-documentation/getting-started/conventions.md) has the complete table. Unwrap in one place rather than inline at each call site.
{% endhint %}

Given classification takes \~13 seconds, the synchronous endpoint is usually the better choice unless you are processing a large backlog.

### Step 3 — Extract with the classified type

```bash
python -c "
import base64, json, sys
content = base64.b64encode(open('unknown.pdf','rb').read()).decode()
json.dump({'documentType': sys.argv[1], 'content': content}, open('extract.json','w'))
" "$TYPE"

curl -X POST "https://api-docai-uat.uptiq.ai/extract" \
  -H "X-Api-Key: $DOCAI_API_KEY" \
  -H "Content-Type: application/json" \
  --data-binary @extract.json
```

Then poll as described in [Extract asynchronously and poll](/document-ai/cookbooks/async-extraction-and-polling.md).

### A shortcut worth knowing

Extraction can classify internally. `analysisDepth` controls it:

| Value                | Behaviour                                                     |
| -------------------- | ------------------------------------------------------------- |
| `standard` (default) | Full analysis, including working out the document's structure |
| `quick`              | Skips classification **when `documentType` is provided**      |

So if you already trust the type, `analysisDepth: "quick"` saves the platform repeating work you have done. Conversely, if you send a document without a confident type, leaving `analysisDepth` at `standard` lets extraction do its own analysis.

The explicit two-step in this recipe is still worth it when you need the classification **decision** as a first-class artifact — to log it, to gate on confidence, or to route to different downstream systems by document category.

### Handling a ZIP of mixed documents

`POST /classify/bulk` takes a set, and a ZIP is classified per member document. Those results carry `classificationGroupId`, `classificationGroupIndex` and `classificationGroupTotal` so you can reassemble the set.

{% hint style="info" %}
Those group fields appear **only** for ZIP uploads. A standard PDF or Excel classification does not carry them, so do not write a handler that requires them.
{% endhint %}

For a full worked example of a mixed packet, see [Process a loan application packet](/document-ai/cookbooks/loan-application-intake.md).

### When it goes wrong

| Symptom                                                | Cause                                                           | Fix                                                                                                            |
| ------------------------------------------------------ | --------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------- |
| Confidence consistently low                            | Poor scan quality                                               | Check `result.documentQuality`; consider re-scanning                                                           |
| Type not in your routing table                         | The registry is larger than you assumed                         | Read it at build time — see [Discovering document types](/document-ai/cookbooks/discovering-document-types.md) |
| `500` on classify                                      | Missing or malformed body — not a server fault on this endpoint | Validate the JSON; see [Errors](/document-ai/developer-documentation/getting-started/errors.md)                |
| Extraction returns few fields despite a confident type | The type is right but the document is sparse                    | Check `extractionMetrics.missingFields`                                                                        |

### Related pages

* [Classification API](/document-ai/developer-documentation/integration-guides/classification.md) — every classification endpoint and parameter.
* [Document Classification](/document-ai/guides-1/document-classification.md) — the same capability in the UI.
* [Document Types](/document-ai/guides-1/document-types.md) — the registry both steps draw on.
* [Process a loan application packet](/document-ai/cookbooks/loan-application-intake.md) — this pattern at packet scale.


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.uptiq.ai/document-ai/cookbooks/classify-then-extract.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
