Extract ACORD fields with Textract Queries

Commercial submission intake is document work: ACORD 125s and 126s, loss runs, supplemental applications, most of them PDFs, some of them photographs of paper. Before anything lands in the AMS, someone reads those documents and keys what they find. Amazon Textract's Queries feature is the most direct tool we know for the reading step: you ask the document a plain-English question and get back an answer with a confidence score and a location on the page.

This tutorial walks through calling Textract Queries from Python against an ACORD form, pairing the answers with the questions, and deciding what to trust. It assumes an AWS account and basic Python.

How Queries work

Textract's AnalyzeDocument API takes a document plus a list of feature types. With the QUERIES feature you also pass natural-language questions, each with an alias. The response is a flat list of Block objects; each question comes back as a QUERY block linked to a QUERY_RESULT block that carries the answer text, a confidence score, and the answer's position on the page. The Queries response documentation shows the exact shape.

Two properties make this a good fit for ACORD forms. First, you do not need a template per form version; the question "What is the named insured?" survives layout changes that break coordinate-based extraction. Second, the confidence score gives you an honest routing signal: high-confidence answers can flow onward, low-confidence ones go to a person.

Set up

Install and authenticate

python -m venv .venv && source .venv/bin/activate
pip install boto3

Credentials come from your usual AWS configuration (environment variables, a profile, or an instance role). The calling identity needs the textract:AnalyzeDocument IAM permission.

Make the first call

Synchronous AnalyzeDocument takes a single-page document of up to 10 MB. That suits page-at-a-time processing; for a full multi-page submission packet, use the asynchronous StartDocumentAnalysis variant with the same queries configuration.

import boto3

textract = boto3.client("textract", region_name="us-east-1")

with open("acord-125-page1.png", "rb") as f:
    document = f.read()

response = textract.analyze_document(
    Document={"Bytes": document},
    FeatureTypes=["QUERIES"],
    QueriesConfig={
        "Queries": [
            {"Text": "What is the named insured?", "Alias": "NAMED_INSURED"},
            {"Text": "What is the proposed effective date?", "Alias": "EFFECTIVE_DATE"},
            {"Text": "What is the applicant's mailing address?", "Alias": "MAILING_ADDRESS"},
            {"Text": "What is the FEIN?", "Alias": "FEIN"},
        ]
    },
)

Read the response

Pair questions with answers

Answers are linked to questions by block relationships, not by list position, so index the blocks by id and follow each query's ANSWER relationship:

def query_answers(blocks):
    by_id = {b["Id"]: b for b in blocks}
    answers = {}
    for block in blocks:
        if block["BlockType"] != "QUERY":
            continue
        alias = block["Query"]["Alias"]
        answers[alias] = None
        for rel in block.get("Relationships", []):
            if rel["Type"] == "ANSWER":
                result = by_id[rel["Ids"][0]]
                answers[alias] = {
                    "text": result.get("Text"),
                    "confidence": result.get("Confidence"),
                }
    return answers

print(query_answers(response["Blocks"]))

A question Textract cannot answer simply has no linked result, which is why the code seeds None: an unanswered query is a signal, not an error, and your pipeline should treat it as "route to a human", never as an empty string.

Choose a confidence policy

Decide, per field, what confidence is enough, and route everything below it to review. A date feeding a renewal deadline deserves a stricter threshold than a contact name. Whatever thresholds you pick, log the score alongside the extracted value so a reviewer can see why something was queued.

Queries that work on ACORD forms

Question phrasing matters more than anything else, and phrasings that echo the form's own labels do best. A starting set for an ACORD 125:

Query textAliasWhere it lands in the AMS
What is the named insured?NAMED_INSUREDAccount or prospect name
What is the applicant's mailing address?MAILING_ADDRESSAccount address
What is the proposed effective date?EFFECTIVE_DATEPolicy effective date
What is the FEIN?FEINAccount tax id
What is the description of operations?OPERATIONSRisk narrative

Treat the aliases as your stable contract: downstream code keys off the alias, and you can rephrase the question text freely while you tune.

Where this fits in an agency pipeline

Extraction is the first third of quote intake. The rest is reconciliation (the ACORD says one thing, the loss runs another), AMS write-back through your management system's API, and the gap list a producer actually wants. And whatever the confidence scores say, a licensed person approves what goes out; the scores decide how much checking a document needs, not whether checking happens.

This is the pipeline our Quote-Intake Agent work builds out. If you are wiring one up for your own agency and want a senior engineer alongside, contact us.