> 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.

# Extract, then verify

[Extract](/capabilities/extract) returns a filled object and nothing else. The response carries no probability, so a wrong value looks exactly like a right one.

[Verify](/capabilities/verify) closes that gap. It re-reads the source text for one field, compares your value against what it finds, and returns the spans it read. Run it before you write an extracted value into a database, a ledger, or a payment.

## The loop

#### Extract the record

Send your JSON Schema to `/v1/decision-machine-1/extract`. You get a typed object back. Missing values are `null`.

#### Verify the fields that carry risk

Send each high-risk field and its extracted value to `/v1/decision-machine-1/verify`. One call per field.

#### Route on the result

Write the value when `matches` is `true` and `probability` is high. Queue the record for review otherwise. Attach `found` to the queue item.

> **Note**
>
> Verify does not correct a value. It reports agreement or disagreement. Your code decides what happens next.

## Step 1: extract

This invoice text drives the whole page.

```
INVOICE #4471
Billed to: Acme Corp
Invoice date: 3 September 2026
Currency: USD
Paid: yes
Total due: $2,676.00
Payment terms: Net 30
```

A four-field schema returns this:

```json
{"data":{"invoice_number":"4471","customer":"Acme Corp","total_due":2676,"currency":"USD"}}
```

Two of these fields move money: `invoice_number` and `total_due`. Verify both.

## Step 2: verify

**`curl`**

```bash title="curl"
curl -s -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\nBilled to: Acme Corp\nInvoice date: 3 September 2026\nCurrency: USD\nPaid: yes\nTotal due: $2,676.00\nPayment terms: Net 30",
    "field": {"name": "total_due", "description": "Total amount due on the invoice"},
    "value": 2676
  }'
```

**`Python (requests)`**

```python title="Python (requests)"
import requests

BASE = "https://api.milliseconds.ai/v1/decision-machine-1"

def verify(text, name, description, value):
    r = requests.post(f"{BASE}/verify", headers={"authorization": "Bearer test_sk-..."}, json={
        "text": text,
        "field": {"name": name, "description": description},
        "value": value,
    })
    r.raise_for_status()
    return r.json()

def check_record(text, data, fields):
    """fields: {name: description}. Returns (clean, review)."""
    review = []
    for name, description in fields.items():
        value = data.get(name)
        if value is None:
            review.append({"field": name, "reason": "missing", "found": []})
            continue
        result = verify(text, name, description, value)
        if not result["matches"] or result["probability"] < 0.7:
            review.append({
                "field": name,
                "reason": "mismatch" if not result["matches"] else "low confidence",
                "value": value,
                "probability": result["probability"],
                "found": result["found"],
            })
    return len(review) == 0, review
```

**`TypeScript (fetch)`**

```typescript title="TypeScript (fetch)"
const BASE = "https://api.milliseconds.ai/v1/decision-machine-1";

type VerifyResult = { matches: boolean; probability: number; found: string[] };

async function verify(
  text: string,
  name: string,
  description: string,
  value: string | number,
): Promise<VerifyResult> {
  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 }),
  });
  if (!res.ok) throw new Error(`verify failed: ${res.status}`);
  return res.json();
}

async function checkRecord(
  text: string,
  data: Record<string, unknown>,
  fields: Record<string, string>,
) {
  const review = [];
  for (const [name, description] of Object.entries(fields)) {
    const value = data[name];
    if (value == null) {
      review.push({ field: name, reason: "missing", found: [] });
      continue;
    }
    const r = await verify(text, name, description, value as string | number);
    if (!r.matches || r.probability < 0.7) {
      review.push({
        field: name,
        reason: r.matches ? "low confidence" : "mismatch",
        value,
        probability: r.probability,
        found: r.found,
      });
    }
  }
  return { clean: review.length === 0, review };
}
```

## Step 3: read the answer

Real responses against the invoice text above:

| Field            | Value sent | Response                                                     |
| ---------------- | ---------- | ------------------------------------------------------------ |
| `total_due`      | `2676`     | `{"matches":true,"probability":0.998,"found":["$2,676.00"]}` |
| `total_due`      | `2500`     | `{"matches":false,"probability":0,"found":["$2,676.00"]}`    |
| `invoice_number` | `"4471"`   | `{"matches":true,"probability":0.638,"found":["4471"]}`      |
| `invoice_number` | `"4417"`   | `{"matches":false,"probability":0,"found":["4471"]}`         |

Three fields carry the decision.

* `matches` is `true` when `probability >= 0.5`.
* `probability` is the confidence of the span that matched. It is `0` when no span matches.
* `found` lists every span verify read for that field, match or not.

A correct value can still score low. The right invoice number scored `0.638` above.

`found` is the field that makes a review queue useful. A human sees `["4471"]` next to your value `4417` and fixes the record in seconds.

## The matching rule

Verify compares canonical forms, and containment counts only from 3 characters up, so short values need exact text. [Verify](/capabilities/verify) states the full rule.

## Routing bands

Set one threshold per field, not one per document. Raise the bar with the blast radius.

| Result                                                             | Action                        |
| ------------------------------------------------------------------ | ----------------------------- |
| `matches` is `true`, `probability` at or above the field threshold | Write the value               |
| `matches` is `true`, `probability` below the field threshold       | Write a draft and flag it     |
| `matches` is `false`                                               | Queue for review with `found` |
| The extracted value is `null`                                      | Queue for review              |

Start at `0.9` for payment amounts and account numbers. Start at `0.7` for references, dates and counterparties. These two numbers are a starting point, not a measured result. The helper above uses one threshold of `0.7`; raise it per field for the values that move money.

The [thresholds page](/concepts/thresholds) explains how to pick these numbers from your own data.

> **Warning**
>
> Verify only checks fields you ask about. One verify call covers one field. List every field that carries risk, and check each one.

## Latency cost

A verify call takes about 0.4 s. An extract call over a short document takes about 0.4 s. Three verify calls in sequence add about 1.2 s. Run them concurrently to keep the wait near one call.

Both calls read the same text. Cost scales with the length of the text plus the schema and the field you send, at \$0.04 per million input tokens and \$0 per output token. See [Pricing](/reference/pricing). Send the smallest span that contains the field. A 2,000-character page beats a 20,000-character bundle.

## Next

* [Verify](/capabilities/verify) — the full request and response shape.
* [Invoice extraction with verification](/recipes/invoice-extraction) — the same loop over an invoice header schema.
* [Confidence routing](/patterns/confidence-routing) — the act, confirm, review and escalate bands.