Pull every field from a handwritten property loss form | Reducto Cookbooks
Studio

Customers

Pricing
Reducto leads independent benchmark on structured extraction with Deep Extract
Parse
Extract
June 29, 2026

Pull every field from a handwritten property loss form

First notice of loss forms come in handwritten, dozens of fields packed into a tight grid with corrections scribbled over the originals. Reducto reads the handwriting, skips the crossed-out edits, and returns every field as structured JSON with a citation on each value.

A property loss notice is often the first document in a claim, and it usually arrives handwritten: dozens of fields crammed into a tight grid, three stacked policy blocks, checkbox phone types, and corrections scribbled over the originals. Plain OCR stumbles on the handwriting and reads the crossed-out text right back to you, and a single transposed policy number can misroute a claim. With Reducto you parse the form once so the handwriting comes back as clean text, then extract every field as structured JSON with a source citation on each value. You describe the fields you need one time and the same pipeline carries over to the next carrier's form.

What you'll build

Handwritten property loss form (PDF, scan, or photo)

  • Reducto Parse with agentic text correction (reads the handwriting)
  • Clean, structured text and form grids (HTML)
  • Reducto Extract with your schema (+ citations)
  • Every field as structured JSON, cited to its source

Setup

Grab an API key from studio.reducto.ai -> API Keys -> Create new API key, then set it and install the SDK:

bash
export REDUCTO_API_KEY="your-api-key-here" pip install reducto

Use any property loss or first-notice-of-loss form: a scan, a PDF, or a phone photo of a handwritten page. No template setup or per-carrier tuning required. Need a sample? Open this handwritten property loss notice in Reducto Studio, where the form is already loaded. Run the pipeline right there, or download it as a PDF to follow the API steps below.

Step 1: Parse with agentic text correction

Handwriting is where most form pipelines break. Turn on agentic text correction so a vision model re-reads the page and rebuilds messy or low-contrast handwriting into clean text, and keep the form's grid as HTML so related fields stay together. Chunking is disabled here because the citations in Step 2 need the full, unchunked parse.

python
from pathlib import Path from reducto import Reducto client = Reducto() upload = client.upload(file=Path("property_loss_notice.pdf")) parse_result = client.parse.run( input=upload.file_id, enhance={ "agentic": [{"scope": "text"}], # vision model re-reads handwriting and messy text "intelligent_ordering": False, }, formatting={ "table_output_format": "html", # keep the form's grid structure intact }, settings={ "extraction_mode": "hybrid", "ocr_system": "standard", }, retrieval={ "chunking": {"chunk_mode": "disabled"}, # citations in Step 2 need the full parse }, ) print(f"Parsed {parse_result.usage.num_pages} pages. Review it in Studio: {parse_result.studio_link}")

Extract can only return what Parse sees, so open the job in Studio and confirm the handwriting came through before you extract.

Step 2: Extract every field

Point Extract at the parse you just ran with a jobid:// reference so it reuses that result instead of parsing again. The schema names each field you want, and the descriptions do the heavy lifting: they tell the model which of the three stacked policy blocks a number belongs to, which person an address belongs to, and to return only the text that is not crossed out. Citations are on, so every value comes back with the page it was read from.

python
schema = { "type": "object", "properties": { "form_date": { "type": "string", "description": "Date the form was filled out, from the header DATE field. Distinct from the date of loss.", }, "agency_customer_id": { "type": "string", "description": "Agency customer ID in the agency block.", }, "agency_code": { "type": "string", "description": "Code value in the agency block, below the email field.", }, "agency_subcode": { "type": "string", "description": "Subcode value in the agency block, to the right of the code.", }, "agency_contact_name": { "type": "string", "description": "Contact name in the agency block, the agency representative rather than the insured's contact person.", }, "agency_contact_phone": { "type": "string", "description": "Phone number in the agency block.", }, "agency_contact_email": { "type": "string", "description": "Email address in the agency block.", }, "insured_location_code": { "type": "string", "description": "Insured location code field near the top center of the form.", }, "date_of_loss": { "type": "string", "description": "Date the loss occurred, from the header. Not the date the form was filled out.", }, "time_of_loss": { "type": "string", "description": "Time the loss occurred, next to the date of loss in the header.", }, "time_of_loss_am_pm": { "type": "string", "enum": ["AM", "PM"], "description": "Whichever of AM or PM is checked next to the time of loss.", }, "property_policy_carrier": { "type": "string", "description": "Carrier name in the property/home policy block, the first of the stacked policy blocks. Do not return the flood or wind carrier.", }, "property_policy_naic_code": { "type": "string", "description": "NAIC code in the property/home policy block (first block).", }, "property_policy_number": { "type": "string", "description": "Policy number in the property/home policy block (first block).", }, "flood_policy_carrier": { "type": "string", "description": "Carrier name in the flood policy block (second block).", }, "flood_policy_naic_code": { "type": "string", "description": "NAIC code in the flood policy block (second block).", }, "flood_policy_number": { "type": "string", "description": "Policy number in the flood policy block (second block).", }, "wind_policy_carrier": { "type": "string", "description": "Carrier name in the wind policy block (third block).", }, "wind_policy_naic_code": { "type": "string", "description": "NAIC code in the wind policy block (third block).", }, "wind_policy_number": { "type": "string", "description": "Policy number in the wind policy block (third block).", }, "insured_name_first": { "type": "string", "description": "Insured's first name, from the name of insured field.", }, "insured_name_middle": { "type": "string", "description": "Insured's middle name or initial, from the name of insured field.", }, "insured_name_last": { "type": "string", "description": "Insured's last name, from the name of insured field.", }, "insured_date_of_birth": { "type": "string", "description": "Date of birth for the primary insured (first row of the insured block, not the spouse row below).", }, "insured_fein": { "type": "string", "description": "FEIN for the primary insured, if applicable, first row of the insured block.", }, "insured_primary_phone": { "type": "string", "description": "Primary phone number for the insured (first row, left phone column).", }, "insured_primary_phone_type": { "type": "string", "enum": ["HOME", "BUS", "CELL"], "description": "Which box (HOME, BUS, or CELL) is checked next to the insured's primary phone.", }, "insured_secondary_phone_type": { "type": "string", "enum": ["HOME", "BUS", "CELL"], "description": "Which box (HOME, BUS, or CELL) is checked next to the insured's secondary phone.", }, "insured_mailing_address": { "type": "string", "description": "Insured's mailing address as a single line. If a value is crossed out and rewritten, return only the text that is not struck through.", }, "insured_primary_email": { "type": "string", "description": "Primary email address for the insured (first row of the insured block).", }, "spouse_date_of_birth": { "type": "string", "description": "Date of birth on the spouse row (second row of the insured block). Do not confuse with the primary insured's date of birth directly above.", }, "spouse_fein": { "type": "string", "description": "FEIN for the spouse, if applicable, spouse row of the insured block.", }, "spouse_mailing_address": { "type": "string", "description": "Spouse's mailing address, if applicable. May match or differ from the insured's mailing address.", }, "spouse_primary_email": { "type": "string", "description": "Spouse's primary email address.", }, "contact_name_first": { "type": "string", "description": "First name in the contact block, the claim contact. Distinct from the insured and from the agency contact.", }, "contact_name_last": { "type": "string", "description": "Last name in the contact block, the claim contact.", }, "contact_primary_phone": { "type": "string", "description": "Primary phone number in the contact block.", }, "contact_primary_phone_type": { "type": "string", "enum": ["HOME", "BUS", "CELL"], "description": "Which box (HOME, BUS, or CELL) is checked next to the contact's primary phone.", }, "contact_mailing_address": { "type": "string", "description": "Contact person's mailing address, typically different from the insured's. Extract the ZIP exactly as written even if it appears truncated.", }, "contact_when_to_contact": { "type": "string", "description": "Free-text when-to-contact field, preferred days and times (for example, 'Weekdays 9-5').", }, "loss_location_street": { "type": "string", "description": "Street address where the loss occurred, from the loss block. Usually different from the insured's mailing address. If a value is crossed out, return only the text that is not struck through.", }, "loss_location_country": { "type": "string", "description": "Country field in the loss block.", }, "police_or_fire_department_contacted": { "type": "string", "description": "Name of the police or fire department contacted, in the loss block.", }, "report_number": { "type": "string", "description": "Report number from the police or fire department, below the department name.", }, "probable_amount_entire_loss": { "type": "string", "description": "Total estimated loss amount, returned with the dollar sign and commas exactly as written.", }, "description_of_loss_and_damage": { "type": "string", "description": "Verbatim transcription of the handwritten description of loss and damage. Preserve the original wording, abbreviations, and punctuation. Do not summarize or split out sub-amounts. Exclude any struck-through text.", }, "reported_by": { "type": "string", "description": "Name in the reported by field.", }, "reported_to": { "type": "string", "description": "Reported to field, typically a department and carrier separated by a comma.", }, }, } result = client.extract.run( input=f"jobid://{parse_result.job_id}", # reuse the Step 1 parse, no re-parsing instructions={ "schema": schema, "system_prompt": ( "This is a property loss notice (first notice of loss). Extract each " "field exactly as written and keep handwriting verbatim. When a value " "is crossed out and rewritten, return only the value that is not " "struck through." ), }, settings={ "citations": {"enabled": True}, }, ) print(f"Fields: {result.usage.num_fields}, pages: {result.usage.num_pages}, credits: {result.usage.credits}")

Step 3: Read the citation-backed output

With citations on, result.result is a dict and each value comes back wrapped as an object with a value and a citations list. Read the data with field["value"] and its source with field["citations"]. Handwritten fields often come back with lower confidence, which is exactly what you want to route to a human.

python
data = result.result print(f"Reported by {data['reported_by']['value']} on {data['form_date']['value']}") print(f"Date of loss: {data['date_of_loss']['value']} ({data['time_of_loss_am_pm']['value']})") print(f"Property policy: {data['property_policy_carrier']['value']} #{data['property_policy_number']['value']}") print(f"Insured: {data['insured_name_first']['value']} {data['insured_name_last']['value']}") print(f"Estimated loss: {data['probable_amount_entire_loss']['value']}") desc = data["description_of_loss_and_damage"] page = desc["citations"][0]["bbox"]["page"] if desc["citations"] else "unknown" confidence = desc["citations"][0]["confidence"] if desc["citations"] else "unknown" print(f"Description (page {page}, {confidence} confidence):") print(desc["value"])

A trimmed example of the structured result (illustrative). Each value is wrapped as {value, citations}, and confidence comes back as "high" or "low":

json
{ "form_date": { "value": "03/15/2026", "citations": [ { "type": "Text", "content": "DATE 03/15/2026", "bbox": { "left": 0.78, "top": 0.06, "width": 0.14, "height": 0.02, "page": 1 }, "confidence": "high" } ] }, "date_of_loss": { "value": "03/12/2026", "citations": [ { "type": "Text", "content": "DATE OF LOSS 03/12/2026", "bbox": { "page": 1 }, "confidence": "high" } ] }, "time_of_loss_am_pm": { "value": "PM", "citations": [ { "type": "Text", "content": "PM", "bbox": { "page": 1 }, "confidence": "high" } ] }, "property_policy_carrier": { "value": "Acme Property Insurers", "citations": [ { "type": "Text", "content": "Acme Property Insurers", "bbox": { "page": 1 }, "confidence": "high" } ] }, "property_policy_number": { "value": "PH-0481922", "citations": [ { "type": "Text", "content": "PH-0481922", "bbox": { "page": 1 }, "confidence": "high" } ] }, "insured_name_last": { "value": "Smith", "citations": [ { "type": "Text", "content": "Smith", "bbox": { "page": 1 }, "confidence": "high" } ] }, "insured_mailing_address": { "value": "789 Elm Avenue, Springfield, IL 62704", "citations": [ { "type": "Text", "content": "789 Elm Avenue, Springfield, IL 62704", "bbox": { "page": 1 }, "confidence": "high" } ] }, "probable_amount_entire_loss": { "value": "$75,000", "citations": [ { "type": "Text", "content": "$75,000", "bbox": { "page": 1 }, "confidence": "high" } ] }, "description_of_loss_and_damage": { "value": "Kitchen fire spread to cabinets and ceiling; smoke damage throughout first floor.", "citations": [ { "type": "Text", "content": "Kitchen fire spread to cabinets...", "bbox": { "page": 1 }, "confidence": "low" } ] } }

Confidence is categorical ("high" or "low") by default; the low-confidence description above is a good candidate to flag for review. Enable numerical_confidence in the citations settings to also get a 0 to 1 score on every value.

Prefer no code?

Want to see it run first? Open this Studio pipeline with the sample form already loaded and press Run. To build your own, in Reducto Studio upload a form, add a Parse action with agentic text correction, chain an Extract action with your fields, and Run. Click any value to highlight its source on the page, then Deploy the pipeline as an API endpoint for the rest of your forms.

Where this goes next

Parse and Extract are two tasks on one platform. Chain the rest of the toolkit onto the same claims pipeline:

  • Sort first with Classify: route property, auto, and liability loss notices to the right schema before extracting.
  • Break apart packets with Split: divide a multi-form claim submission into separate documents.
  • Reach for Deep Extract: when the handwriting is rough or a field has to be exact, its self-verifying mode re-reads until it is confident.
  • Add humans where it matters with Pipelines: hold low-confidence handwritten fields for review before they post to the claims system.
CTA patternReducto logo

Make your first API call in minutes.

Reducto logoLLM Center