> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.milliseconds.ai/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.milliseconds.ai/_mcp/server.

# Invoice extraction with verification

An invoice pipeline needs two capabilities. [Extract](/capabilities/extract) fills your schema. [Verify](/capabilities/verify) checks one value against the text before you write it to a ledger.

This recipe uses one plain-text invoice. Convert your PDF to text first: the API reads text, not files. Every request needs an API key. See [Authentication](/authentication).

**`invoice.txt`**

```text title="invoice.txt"
INVOICE #4471
Northwind Studio Ltd
Billed to: Acme Corp
Invoice date: 3 September 2026
Currency: USD
Payment terms: Net 30
Total due: $2,676.00
```

#### Extract the invoice header

Send the text and a JSON Schema. Describe every field: the description drives the match.

**`curl`**

```bash title="curl"
curl -X POST https://api.milliseconds.ai/v1/decision-machine-1/extract \
  -H "Content-Type: application/json" \
  -H "authorization: Bearer test_sk-..." \
  -d '{
    "text": "INVOICE #4471\nNorthwind Studio Ltd\nBilled to: Acme Corp\nInvoice date: 3 September 2026\nCurrency: USD\nPayment terms: Net 30\nTotal due: $2,676.00",
    "schema": {
      "title": "invoice",
      "type": "object",
      "properties": {
        "invoice_number": { "type": "string", "description": "invoice number" },
        "customer": { "type": "string", "description": "company billed" },
        "invoice_date": { "type": "string", "description": "invoice date" },
        "total_due": { "type": "number", "description": "total amount due" },
        "net_terms_days": { "type": "integer", "description": "payment terms in days" }
      }
    }
  }'
```

**`Python`**

```python title="Python"
import requests

BASE = "https://api.milliseconds.ai/v1/decision-machine-1"
text = open("invoice.txt").read()

schema = {
    "title": "invoice",
    "type": "object",
    "properties": {
        "invoice_number": {"type": "string", "description": "invoice number"},
        "customer": {"type": "string", "description": "company billed"},
        "invoice_date": {"type": "string", "description": "invoice date"},
        "total_due": {"type": "number", "description": "total amount due"},
        "net_terms_days": {"type": "integer", "description": "payment terms in days"},
    },
}

data = requests.post(f"{BASE}/extract", headers={"authorization": "Bearer test_sk-..."}, json={"text": text, "schema": schema}).json()["data"]
```

**`TypeScript`**

```typescript title="TypeScript"
import { readFile } from "node:fs/promises";

const BASE = "https://api.milliseconds.ai/v1/decision-machine-1";
const text = await readFile("invoice.txt", "utf8");

const schema = {
  title: "invoice",
  type: "object",
  properties: {
    invoice_number: { type: "string", description: "invoice number" },
    customer: { type: "string", description: "company billed" },
    invoice_date: { type: "string", description: "invoice date" },
    total_due: { type: "number", description: "total amount due" },
    net_terms_days: { type: "integer", description: "payment terms in days" },
  },
};

const res = await fetch(`${BASE}/extract`, {
  method: "POST",
  headers: { "Content-Type": "application/json", authorization: "Bearer test_sk-..." },
  body: JSON.stringify({ text, schema }),
});
const { data } = await res.json();
```

The real response:

```json
{
  "data": {
    "invoice_number": "4471",
    "customer": "Northwind Studio Ltd",
    "invoice_date": "3 September 2026",
    "total_due": 2676,
    "net_terms_days": 30
  }
}
```

Read it honestly. The invoice number, the date, the integer and the money field are right. `customer` is wrong: it repeats the vendor name instead of the billed company. Treat the whole record as a draft.

#### Verify the fields that matter

`verify` takes one field and one value. It reports what the text says for that field. Run it on the fields that carry money or identity.

**`curl`**

```bash title="curl"
curl -X POST https://api.milliseconds.ai/v1/decision-machine-1/verify \
  -H "Content-Type: application/json" \
  -H "authorization: Bearer test_sk-..." \
  -d '{
    "text": "INVOICE #4471\nNorthwind Studio Ltd\nBilled to: Acme Corp\nInvoice date: 3 September 2026\nCurrency: USD\nPayment terms: Net 30\nTotal due: $2,676.00",
    "field": { "name": "total_due", "description": "total amount due" },
    "value": "2676"
  }'
```

**`Python`**

```python title="Python"
def verify(text, name, description, value):
    body = {"text": text, "field": {"name": name, "description": description}, "value": str(value)}
    return requests.post(f"{BASE}/verify", headers={"authorization": "Bearer test_sk-..."}, json=body).json()

checks = {
    "invoice_number": verify(text, "invoice_number", "invoice number", data["invoice_number"]),
    "total_due": verify(text, "total_due", "total amount due", data["total_due"]),
}
```

**`TypeScript`**

```typescript title="TypeScript"
async function verify(text: string, name: string, description: string, value: unknown) {
  const res = await fetch(`${BASE}/verify`, {
    method: "POST",
    headers: { "Content-Type": "application/json", authorization: "Bearer test_sk-..." },
    body: JSON.stringify({ text, field: { name, description }, value: String(value) }),
  });
  return res.json();
}

const checks = {
  invoice_number: await verify(text, "invoice_number", "invoice number", data.invoice_number),
  total_due: await verify(text, "total_due", "total amount due", data.total_due),
};
```

Three real calls against the same invoice. Row three sends a wrong total on purpose.

| Field and value           | Response                                                     |
| ------------------------- | ------------------------------------------------------------ |
| `invoice_number` = `4471` | `{"matches":true,"probability":0.65,"found":["4471"]}`       |
| `total_due` = `2676`      | `{"matches":true,"probability":0.997,"found":["$2,676.00"]}` |
| `total_due` = `2675`      | `{"matches":false,"probability":0,"found":["$2,676.00"]}`    |

Three rules come out of this table.

* `matches` is `true` when `probability` is 0.5 or higher. A wrong total drops the probability to `0`.
* `found` holds the spans the text really carries for that field. It is your debugging field.
* The comparison lowercases both sides and removes every non-alphanumeric character. `$2,676.00` matches `2676`. Containment counts only when the canonical value has 3 or more characters.

> **Note**
>
> More than one span in `found` means the field is ambiguous, not confirmed. Send those records to review.

#### Build the review queue

Combine the extracted value and the verify result into one decision per field. Write only the fields that pass.

**`Python`**

```python title="Python"
ACT = 0.90  # write without a person

def route(value, check):
    if value is None:
        return "review"  # nothing extracted
    if not check["matches"]:
        return "reject"  # the text says something else
    if len(check["found"]) > 1:
        return "review"  # ambiguous field
    return "write" if check["probability"] >= ACT else "review"

queue = {f: route(data.get(f), c) for f, c in checks.items()}
# {'invoice_number': 'review', 'total_due': 'write'}
```

**`TypeScript`**

```typescript title="TypeScript"
const ACT = 0.9;

function route(value: unknown, check: { matches: boolean; probability: number; found: string[] }) {
  if (value === null || value === undefined) return "review";
  if (!check.matches) return "reject";
  if (check.found.length > 1) return "review";
  return check.probability >= ACT ? "write" : "review";
}
```

The invoice number lands at 0.65 and goes to review. The total lands at 0.997 and writes straight through. Raise `ACT` for fields that move money. Pick the numbers from your own data with [tuning thresholds](/evaluate/tuning-thresholds), not from this page.

## Cost and limits

* One invoice costs one `extract` call plus one `verify` call per checked field.
* Measured latency: `extract` near 0.41 s, `verify` near 0.40 s.
* `text` accepts up to 20,000 characters. [Long text](/concepts/long-text) is chunked at 2,000 characters. Pre-split longer invoices yourself.
* Send up to 32 invoices in one `texts` array. Results come back in input order. Each text costs one inference call.
* Send `statements` or `questions` in a batch when you also screen the document with [yes-no](/capabilities/yes-no) or [answer](/capabilities/answer). Extra statements and questions add no inference call; each adds only its own length to the billed input.

## Next

* [Extract, then verify](/patterns/extract-then-verify) — the general pattern behind this recipe.
* [Extract](/capabilities/extract) — the full JSON Schema support matrix.
* [Confidence routing](/patterns/confidence-routing) — act, confirm, review and escalate bands.