Skip to content

Fill forms from email

Fill a form from an email. Fields it cannot find stay empty and are flagged.

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.

  • Extract
Try
  1. Read every field of the claim formExtract
    Policyholder
    Daniel Okoro
    Policy number
    AU-77120-K
    Phone
    07700 900123
    Date of incident
    2026-10-16
    Time of incident
    18:40
    Vehicle
    Aurora C5
    Registration
    AB12 CDE
    Damage
    rear bumper, boot lid
    Repair estimate
    £2,340.00
    Police report number
    missing
    A missing field stays empty. A field Sage2 is unsure of gets a check, for a person to confirm.96 ms
  2. Your codeClaim form filled

    The application saves 9 fields and drafts the reply: “Thanks, Daniel. Could you send us the police report number?”

One Extract request read all 10 fields: 96 ms.

Get the code

Set SAGE_API_KEY (get a free key), then run it. Python: pip install requests. TypeScript: Node 22+. Save as recipe.mts and run npx tsx recipe.mts.

A null answer is not an error. Sage2 isn't sure, and each step below says what to do with it. Limits, errors and retries: Limits and errors.

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")

How it works

  1. Extract

    Describe the form

    Extract docs

    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.

    Runs with the helper from Get the code.

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

    Read the email

    Extract docs

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

    Runs with the helper from Get the code.

    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"]
    Example Sage2 output
    {
      "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. Your code

    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.

    Runs with the helper from Get the code.

    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")

Before you ship

  1. 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.
  2. 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.
  3. Show where each value came from. text, start and end point at the exact words. A reviewer can check each field against them.
  4. Set a bar per field. A wrong policy number costs more than a misspelled car model.
  5. Fast and small. One call fills this form in about 0.15 s. It reads about 460 tokens and returns about 900.
  6. Many emails at once? /v1/extract/batch takes up to 64 contents with one extract.

Sage / Levanto

Start free.

Sign in or create an account.

Continue with Google
or with email

We’ll email you a 6-digit code.