> 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/loan-application-intake.md).

# Process a loan application packet

**Goal.** Take the pile of documents a borrower submits — financial statements, tax returns, bank statements, incorporation papers, IDs — and turn it into one structured application record, without asking a human to sort it first.

This is the composition recipe. It reuses [Classify, then extract](/document-ai/cookbooks/classify-then-extract.md) and [Extract many documents at once](/document-ai/cookbooks/bulk-extraction.md) at packet scale, and the interesting parts are the routing and the assembly rather than any single call.

### Before you start

* An API key, exported as `$DOCAI_API_KEY`.
* A packet — individual files, or a ZIP.
* A publicly reachable HTTPS endpoint if you want push completion, per [Build an event-driven pipeline](/document-ai/cookbooks/event-driven-pipeline.md). Recommended: a packet is a lot of \~90-second extractions.

### The shape of the problem

A packet arrives with no reliable labelling. Filenames are `scan_004.pdf`. One PDF may hold three documents. Some types matter to your credit decision and some are supporting material.

So the pipeline is: **identify everything → decide what to extract → extract in parallel → assemble → review what is weak or missing.**

### Step 1 — Classify everything

```bash
python -c "
import base64, glob, json
files = [{'content': base64.b64encode(open(p,'rb').read()).decode()}
         for p in sorted(glob.glob('packet/*.pdf'))]
json.dump({'files': files}, open('classify-bulk.json','w'))
"

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

{% hint style="info" %}
`POST /classify/bulk` also takes `metadata` and `context` arrays. Both **must be the same length as `files`** — use `metadata` to carry your own filename or document ID through, so you can correlate results back to source files afterwards.
{% endhint %}

For a ZIP, each member is classified separately and the results carry `classificationGroupId`, `classificationGroupIndex` and `classificationGroupTotal`.

### Step 2 — Route on type and confidence

Map classified types onto what your process does with them:

```python
EXTRACT_FOR_SPREADING = {
    "BalanceSheet", "ProfitAndLossStatement",
    "BusinessFinancialStatement", "AuditedBusinessFinancialStatement",
}
EXTRACT_FOR_VERIFICATION = {"BankStatements", "ArticlesOfIncorporation", "W9"}
FILE_ONLY               = {"AuditNotes"}

CONFIDENCE_FLOOR = 0.80

def route(result):
    doc_type   = result["type"]
    confidence = result["confidence"]

    if confidence < CONFIDENCE_FLOOR:
        return "review", doc_type
    if doc_type in EXTRACT_FOR_SPREADING:
        return "spread", doc_type
    if doc_type in EXTRACT_FOR_VERIFICATION:
        return "verify", doc_type
    if doc_type in FILE_ONLY:
        return "file", doc_type
    return "review", doc_type          # unknown type -> human
```

{% hint style="warning" %}
Default unrecognised types to review, never to discard. The registry is larger than any routing table you write by hand, and it grows without a release — see [Discovering document types](/document-ai/cookbooks/discovering-document-types.md). A document silently dropped because nobody added its type is the failure mode that actually hurts.
{% endhint %}

### Step 3 — Extract what matters

Submit the routed documents as a batch, each with the type classification found:

```python
files = [
    {"documentType": doc_type,
     "content": encoded[path],
     "metadata": {"sourceFile": path, "applicationId": APP_ID}}
    for path, doc_type in to_extract
]
submit("/extract/bulk", {"files": files})
```

Setting `metadata` per file matters here. Group results come back **keyed by document type, not by submission order**, so `metadata` is how you get back to "which file was this".

Pass `industry` on financial documents to load a default chart of accounts — see [Spread a set of financials](/document-ai/cookbooks/financial-spreading.md).

### Step 4 — Assemble the record

Collect children as they complete, whether by webhook or by `GET /document-extractions/group/{id}`:

```python
application = {"applicationId": APP_ID, "documents": [], "flags": []}

for child in children:
    metrics = child["result"]["extractionMetrics"]
    application["documents"].append({
        "type":       child["documentType"],
        "sourceFile": child.get("metadata", {}).get("sourceFile"),
        "data":       child["result"]["extractedData"],
        "accuracy":   metrics["accuracyScore"],
        "missing":    metrics["missingFields"],
    })
    if metrics["accuracyScore"] < 80:
        application["flags"].append(f"low confidence: {child['documentType']}")
```

### Step 5 — Check completeness against your policy

The packet being processed is not the same as the packet being complete. Check what you require, not just what arrived:

```python
REQUIRED = {"BalanceSheet", "ProfitAndLossStatement", "BankStatements"}
present  = {d["type"] for d in application["documents"]}

for missing in REQUIRED - present:
    application["flags"].append(f"missing required document: {missing}")
```

This is where the value shows up. A packet missing its P\&L is identified in seconds rather than at the end of a credit review.

### Step 6 — Screen for document integrity

For anything that carries weight in the decision, check the fraud signals on the extraction record before you rely on the numbers:

```python
fraud  = child.get("fraudResults", {})
failed = [r for r in fraud.get("preflight", {}).get("results", [])
          if r["status"] == "FAIL"]
if failed:
    application["flags"].append(
        f"{child['documentType']}: {len(failed)} integrity check(s) failed")
```

Remember that fraud analysis finishes **after** extraction does — see [Reading fraud and control checks](/document-ai/cookbooks/fraud-and-control-checks.md) before wiring this into a gate.

### What a good outcome looks like

| Outcome                              | Meaning                                                 |
| ------------------------------------ | ------------------------------------------------------- |
| No flags, all required types present | Straight through to the credit process                  |
| Low-confidence extractions           | Route those documents to review, keep the rest          |
| Missing required documents           | Go back to the borrower immediately, with specifics     |
| Failed integrity checks              | Hold for the risk team                                  |
| Unrecognised types                   | Human triage — and consider extending the routing table |

### Related pages

* [Classify, then extract](/document-ai/cookbooks/classify-then-extract.md) — the two-step this recipe scales up.
* [Extract many documents at once](/document-ai/cookbooks/bulk-extraction.md) — batch submission and group retrieval.
* [Spread a set of financials](/document-ai/cookbooks/financial-spreading.md) — what happens next with the financial documents.
* [Reading fraud and control checks](/document-ai/cookbooks/fraud-and-control-checks.md) — the integrity gate.
* [Document Types](/document-ai/guides-1/document-types.md) — the registry your routing table draws on.


---

# 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/loan-application-intake.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.
