# Fill forms from email

Fill every form field from an email. Missing fields stay empty and unsure ones are flagged for a person.

Category: Read documents. Live demo: https://levanto.ai/examples/form-filling

Claims, bookings and applications still arrive as emails that someone retypes into a form. This guide fills the whole form with **one Sage2 call**, typed and ready to save.

The useful part is what it doesn't do: a field that isn't in the email stays empty, and a field Sage2 isn't sure of is **flagged for a person**.

## Setup

Base URL: `https://sage.levanto.ai`. Send `Authorization: Bearer $SAGE_API_KEY` on every call. Get a key, free, at https://platform.levanto.ai. Keep the key on your server.

A `null` answer is not an error: Sage2 isn't sure. Each step says what to do with it.

## Step by step

### 1. Describe the form

Each form field becomes a field in `extract`: a name and a short definition. Add a `type` where your form needs one:

- `incident_date` has type `date`, `incident_time` has type `time`.
- `repair_estimate` has type `money`. It comes back as `{amount, currency}`.
- `damage` is a list. You get every damaged part.

```python
FORM = {
    "policyholder": "the person making the claim",
    "policy_number": "the insurance policy number",
    "phone": "the phone number of the person making the claim",
    "incident_date": {"type": "date", "definition": "the date the accident happened"},
    "incident_time": {"type": "time", "definition": "the time the accident happened"},
    "vehicle": "the make and model of the policyholder's car",
    "registration": "the policyholder's car registration",
    "damage": ["a part of the policyholder's car that was damaged"],
    "repair_estimate": {"type": "money", "definition": "the quoted cost of the repair"},
    "police_report": "the police report or crime reference number",
}
```

```typescript
const FORM = {
  policyholder: "the person making the claim",
  policy_number: "the insurance policy number",
  phone: "the phone number of the person making the claim",
  incident_date: { type: "date", definition: "the date the accident happened" },
  incident_time: { type: "time", definition: "the time the accident happened" },
  vehicle: "the make and model of the policyholder's car",
  registration: "the policyholder's car registration",
  damage: ["a part of the policyholder's car that was damaged"],
  repair_estimate: { type: "money", definition: "the quoted cost of the repair" },
  police_report: "the police report or crime reference number",
};
```

### 2. Read the email

Send the email to `/v1/extract` and read `results`. Each field gives the value, the exact `text` it came from and a `probability`.

```python
email = """Hi,

I need to report an accident. My name is Daniel Okoro and my policy number is AU-77120-K.

On Friday 16 October 2026, at around 6:40 pm, I was stopped at the lights on Baker Street when a white van went into the back of my car. It's an Aurora C5, registration AB12 CDE.

The van driver was Sam Whitfield. Nobody was hurt, thankfully, but the rear bumper and the boot lid are smashed. The garage quoted £2,340 to fix it.

A police officer came but I didn't get a report number. They said they would send it by post.

Thanks,
Daniel
07700 900123"""

results = sage("/v1/extract", {"content": email, "extract": FORM})["results"]
```

```typescript
const email = `Hi,

I need to report an accident. My name is Daniel Okoro and my policy number is AU-77120-K.

On Friday 16 October 2026, at around 6:40 pm, I was stopped at the lights on Baker Street when a white van went into the back of my car. It's an Aurora C5, registration AB12 CDE.

The van driver was Sam Whitfield. Nobody was hurt, thankfully, but the rear bumper and the boot lid are smashed. The garage quoted £2,340 to fix it.

A police officer came but I didn't get a report number. They said they would send it by post.

Thanks,
Daniel
07700 900123`;

const results = (await sage("/v1/extract", { content: email, extract: FORM })).results;
```

Response, abridged (a real run):

```json
{
  "values": {
    "policyholder": "Daniel Okoro",
    "policy_number": "AU-77120-K",
    "phone": "07700 900123",
    "incident_date": "2026-10-16",
    "incident_time": "18:40",
    "vehicle": "Aurora C5",
    "registration": "AB12 CDE",
    "damage": [
      "rear bumper",
      "boot lid"
    ],
    "repair_estimate": {
      "amount": 2340,
      "currency": "GBP"
    },
    "police_report": null
  },
  "results": {
    "policyholder": {
      "value": "Daniel Okoro",
      "text": "Daniel Okoro",
      "start": 46,
      "end": 58,
      "probability": 0.9328,
      "probabilities": [
        {
          "value": "Daniel Okoro",
          "text": "Daniel Okoro",
          "start": 46,
          "end": 58,
          "probability": 0.9328
        },
        {
          "value": "Daniel",
          "text": "Daniel",
          "start": 529,
          "end": 535,
          "probability": 0.055
        }
      ]
    },
    "policy_number": {
      "value": "AU-77120-K",
      "text": "AU-77120-K",
      "start": 83,
      "end": 93,
      "probability": 0.9941,
      "probabilities": [
        {
          "value": "AU-77120-K",
          "text": "AU-77120-K",
          "start": 83,
          "end": 93,
          "probability": 0.9941
        },
        {
          "value": null,
          "probability": 0.0059
        }
      ]
    },
    "…": "one entry per field"
  },
  "meta": {
    "model": "levanto-sage-2",
    "usage": {
      "input_tokens": 458,
      "output_tokens": 921
    },
    "latency_ms": 96.2
  }
}
```

### 3. Fill the form and flag the gaps

Each field lands in one of three cases:

- **Found**: `value` is filled and `probability` says how likely it is. Below your bar (0.6 here), flag it for a check. A list field like `damage` has a `probability` per item.
- **Not in the email**: `value` is `null` and `probability` is how likely it's absent. Ask the customer for it.
- **Too close to call**: `value` and `probability` are both `null`. A person checks it.

This email has no police report number. The reply asks for it.

```python
for field, r in results.items():
    if "values" in r:  # a field with every mention
        sure = all((v["probability"] or 0) >= 0.6 for v in r["values"])
        print(field, [v["value"] for v in r["values"]], "" if sure else "-> check")
    elif r["value"] is None:
        print(field, "-> ask the customer" if r["probability"] is not None else "-> check: too close to call")
    else:
        print(field, r["value"], "" if r["probability"] >= 0.6 else "-> check")
```

```typescript
for (const [field, r] of Object.entries<any>(results)) {
  if ("values" in r) {  // a field with every mention
    const sure = r.values.every((v: any) => (v.probability ?? 0) >= 0.6);
    console.log(field, r.values.map((v: any) => v.value), sure ? "" : "-> check");
  } else if (r.value === null) {
    console.log(field, r.probability !== null ? "-> ask the customer" : "-> check: too close to call");
  } else {
    console.log(field, r.value, r.probability >= 0.6 ? "" : "-> check");
  }
}
```

## Before you ship

- **Check it's a claim first.** An email asking "does my policy cover chips?" still fills the form. Ask a Yes/No first ("Is this a new claim?") and fill only on yes.
- **Watch partial values and close calls.** A date with no year comes back as `--11-04`. When the email has two candidates, `probabilities` lists both: flag the field when the runner-up is close.
- **Show where each value came from.** `text`, `start` and `end` point at the exact words. A reviewer can check each field against them.
- **Set a bar per field.** A wrong policy number costs more than a misspelled car model.
- **Fast and small.** One call fills this form in about 0.15 s. It reads about 460 tokens and returns about 900.
- **Many emails at once?** `/v1/extract/batch` takes up to 64 `contents` with one `extract`.

## Full recipe (Python, `pip install requests`)

```python
import os
import requests

def sage(path, body):
    r = requests.post(
        "https://sage.levanto.ai" + path,
        headers={"Authorization": f"Bearer {os.environ['SAGE_API_KEY']}"},
        json=body,
        timeout=30,
    )
    r.raise_for_status()
    return r.json()

FORM = {
    "policyholder": "the person making the claim",
    "policy_number": "the insurance policy number",
    "phone": "the phone number of the person making the claim",
    "incident_date": {"type": "date", "definition": "the date the accident happened"},
    "incident_time": {"type": "time", "definition": "the time the accident happened"},
    "vehicle": "the make and model of the policyholder's car",
    "registration": "the policyholder's car registration",
    "damage": ["a part of the policyholder's car that was damaged"],
    "repair_estimate": {"type": "money", "definition": "the quoted cost of the repair"},
    "police_report": "the police report or crime reference number",
}

email = """Hi,

I need to report an accident. My name is Daniel Okoro and my policy number is AU-77120-K.

On Friday 16 October 2026, at around 6:40 pm, I was stopped at the lights on Baker Street when a white van went into the back of my car. It's an Aurora C5, registration AB12 CDE.

The van driver was Sam Whitfield. Nobody was hurt, thankfully, but the rear bumper and the boot lid are smashed. The garage quoted £2,340 to fix it.

A police officer came but I didn't get a report number. They said they would send it by post.

Thanks,
Daniel
07700 900123"""

results = sage("/v1/extract", {"content": email, "extract": FORM})["results"]

for field, r in results.items():
    if "values" in r:  # a field with every mention
        sure = all((v["probability"] or 0) >= 0.6 for v in r["values"])
        print(field, [v["value"] for v in r["values"]], "" if sure else "-> check")
    elif r["value"] is None:
        print(field, "-> ask the customer" if r["probability"] is not None else "-> check: too close to call")
    else:
        print(field, r["value"], "" if r["probability"] >= 0.6 else "-> check")
```

## Full recipe (TypeScript, Node 22+, nothing to install: save as `recipe.mts`, run `npx tsx recipe.mts`)

```typescript
// Node 22+. Save as recipe.mts and run npx tsx recipe.mts
async function sage(path: string, body: unknown): Promise<any> {
  const r = await fetch("https://sage.levanto.ai" + path, {
    method: "POST",
    headers: { Authorization: `Bearer ${process.env.SAGE_API_KEY}`, "Content-Type": "application/json" },
    body: JSON.stringify(body),
    signal: AbortSignal.timeout(30_000),
  });
  if (!r.ok) throw new Error(`${r.status} ${await r.text()}`);
  return r.json();
}

const FORM = {
  policyholder: "the person making the claim",
  policy_number: "the insurance policy number",
  phone: "the phone number of the person making the claim",
  incident_date: { type: "date", definition: "the date the accident happened" },
  incident_time: { type: "time", definition: "the time the accident happened" },
  vehicle: "the make and model of the policyholder's car",
  registration: "the policyholder's car registration",
  damage: ["a part of the policyholder's car that was damaged"],
  repair_estimate: { type: "money", definition: "the quoted cost of the repair" },
  police_report: "the police report or crime reference number",
};

const email = `Hi,

I need to report an accident. My name is Daniel Okoro and my policy number is AU-77120-K.

On Friday 16 October 2026, at around 6:40 pm, I was stopped at the lights on Baker Street when a white van went into the back of my car. It's an Aurora C5, registration AB12 CDE.

The van driver was Sam Whitfield. Nobody was hurt, thankfully, but the rear bumper and the boot lid are smashed. The garage quoted £2,340 to fix it.

A police officer came but I didn't get a report number. They said they would send it by post.

Thanks,
Daniel
07700 900123`;

const results = (await sage("/v1/extract", { content: email, extract: FORM })).results;

for (const [field, r] of Object.entries<any>(results)) {
  if ("values" in r) {  // a field with every mention
    const sure = r.values.every((v: any) => (v.probability ?? 0) >= 0.6);
    console.log(field, r.values.map((v: any) => v.value), sure ? "" : "-> check");
  } else if (r.value === null) {
    console.log(field, r.probability !== null ? "-> ask the customer" : "-> check: too close to call");
  } else {
    console.log(field, r.value, r.probability >= 0.6 ? "" : "-> check");
  }
}
```

## Requests

### 1. POST /v1/extract

```bash
curl -s https://sage.levanto.ai/v1/extract \
  -H "Authorization: Bearer $SAGE_API_KEY" \
  -H "Content-Type: application/json" \
  -d @- <<'JSON'
{
  "content": "Hi,\n\nI need to report an accident. My name is Daniel Okoro and my policy number is AU-77120-K.\n\nOn Friday 16 October 2026, at around 6:40 pm, I was stopped at the lights on Baker Street when a white van went into the back of my car. It's an Aurora C5, registration AB12 CDE.\n\nThe van driver was Sam Whitfield. Nobody was hurt, thankfully, but the rear bumper and the boot lid are smashed. The garage quoted £2,340 to fix it.\n\nA police officer came but I didn't get a report number. They said they would send it by post.\n\nThanks,\nDaniel\n07700 900123",
  "extract": {
    "policyholder": "the person making the claim",
    "policy_number": "the insurance policy number",
    "phone": "the phone number of the person making the claim",
    "incident_date": {
      "type": "date",
      "definition": "the date the accident happened"
    },
    "incident_time": {
      "type": "time",
      "definition": "the time the accident happened"
    },
    "vehicle": "the make and model of the policyholder's car",
    "registration": "the policyholder's car registration",
    "damage": [
      "a part of the policyholder's car that was damaged"
    ],
    "repair_estimate": {
      "type": "money",
      "definition": "the quoted cost of the repair"
    },
    "police_report": "the police report or crime reference number"
  }
}
JSON
```

Response, abridged (a real run):

```json
{
  "values": {
    "policyholder": "Daniel Okoro",
    "policy_number": "AU-77120-K",
    "phone": "07700 900123",
    "incident_date": "2026-10-16",
    "incident_time": "18:40",
    "vehicle": "Aurora C5",
    "registration": "AB12 CDE",
    "damage": [
      "rear bumper",
      "boot lid"
    ],
    "repair_estimate": {
      "amount": 2340,
      "currency": "GBP"
    },
    "police_report": null
  },
  "results": {
    "policyholder": {
      "value": "Daniel Okoro",
      "text": "Daniel Okoro",
      "start": 46,
      "end": 58,
      "probability": 0.9328,
      "probabilities": [
        {
          "value": "Daniel Okoro",
          "text": "Daniel Okoro",
          "start": 46,
          "end": 58,
          "probability": 0.9328
        },
        {
          "value": "Daniel",
          "text": "Daniel",
          "start": 529,
          "end": 535,
          "probability": 0.055
        }
      ]
    },
    "policy_number": {
      "value": "AU-77120-K",
      "text": "AU-77120-K",
      "start": 83,
      "end": 93,
      "probability": 0.9941,
      "probabilities": [
        {
          "value": "AU-77120-K",
          "text": "AU-77120-K",
          "start": 83,
          "end": 93,
          "probability": 0.9941
        },
        {
          "value": null,
          "probability": 0.0059
        }
      ]
    },
    "…": "one entry per field"
  },
  "meta": {
    "model": "levanto-sage-2",
    "usage": {
      "input_tokens": 458,
      "output_tokens": 921
    },
    "latency_ms": 96.2
  }
}
```

## Docs

- Extract: https://docs.levanto.ai/extract
- Limits and errors: https://docs.levanto.ai/decision-model/limits
- OpenAPI: https://sage.levanto.ai/openapi.json
