ACORD forms carry the data insurance operations run on: who is insured, by which carriers, under which policies, with which limits, and until when. Most teams still key that data in by hand from PDFs and scans, or maintain templates that break with every new edition and agency layout. This guide shows how to extract structured data from ACORD 25 certificates of liability insurance and ACORD 27 evidence of property insurance with Extend's Parse and Extract APIs, including a complete sample schema, validation rules, and the same approach for ACORD 125 and 126 applications.
The short answer
To extract data from an ACORD form with Extend, upload the form, call the Extract API with a JSON schema that describes the fields you need, and turn on citations. Extract runs Parse under the hood, so checkboxes, insurer letters, and coverage grids are read from layout-aware output rather than raw OCR text. Every value returns with confidence scores and a citation to the page and region it came from. Validate dates and limits in code, and send low-confidence fields to human review before they reach a policy or compliance system.
To skip the setup, start from a ready-made Extend template: the Certificate of Liability Insurance Extractor for ACORD 25, or the ACORD Form Classifier for ACORD 125, 126, and 140 applications.
Key takeaways
- ACORD forms are hard for templates because the answers live in checkboxes, letter codes, and dense limit grids, not in labeled text fields.
- Model the form the way it works: an insurers array keyed by letter, and a coverages array where each line references its insurer by letter.
- Use Extend's
date,currency, andsignaturefield types so dates come back as ISO dates, limits as amounts with currency, and the signature block as a structured object. - Turn on citations. A reviewer can check any value against the exact region of the certificate it came from.
- Validate before you trust: expiration dates, required limits, additional insured status, and the certificate holder name.
ACORD forms this guide covers
| Form | Name | What teams extract |
|---|---|---|
| ACORD 25 | Certificate of Liability Insurance | Producer, insured, insurers and NAIC numbers, coverage lines, policy numbers and dates, limits, additional insured and waiver of subrogation, certificate holder |
| ACORD 27 | Evidence of Property Insurance | Company, policy and loan numbers, dates, property location, perils, amounts of insurance, deductibles, additional interest |
| ACORD 125 | Commercial Insurance Application | Applicant, agency, carrier, policy and transaction details, locations, prior carrier information |
| ACORD 126 | Commercial General Liability Section | Coverage options, limits, deductibles, hazard and classification details |
| ACORD 140 | Property Section | Locations, building details, values, and coverages |
Certificates of insurance (ACORD 25 and 27) are evidence of coverage, not the policy itself. They are read by vendor compliance, property management, lending, and construction teams that need to confirm coverage before work starts or a loan closes. Applications (ACORD 125, 126, and 140) are read by carriers and MGAs during submission intake and underwriting.
Start from an Extend template
Extend publishes two ready-made pipelines for ACORD forms. Each template page runs the pipeline end to end on a real document, shows the configuration for every step, and lets you run your own document for free.
| Template | Pipeline | Use it for |
|---|---|---|
| Certificate of Liability Insurance Extractor | Parse → Extract | ACORD 25 certificates: insured and certificate holder details, producer contact information, insurers, coverage details, and certificate information |
| ACORD Form Classifier | Parse → Classify → Extract | Commercial insurance applications such as ACORD 125, 126, and 140: confirms the document is an ACORD form, then extracts applicant and agency details, coverage selections, policy terms, and dates with a 55-field schema |
Both templates parse with parse_performance and agentic text correction, then extract with extraction_performance, advanced multimodal processing, and the Review Agent turned on. Each one ships with example code in TypeScript, Python, Java, and Go, the workflow JSON, and a Skill.md, and can be sent straight to Claude Code or OpenAI Codex. Use a template to get a working pipeline in minutes, then adapt its schema with the field design in the steps below.
Why ACORD forms are hard to extract
- Checkboxes carry the meaning. CLAIMS-MADE or OCCUR, ADDL INSD, SUBR WVD, and the PERILS INSURED boxes on ACORD 27 decide what a policy covers. An extractor that reads only text misses them.
- Letters link the rows. Each coverage line names its carrier by INSR LTR, A through F. The carrier name and NAIC number sit in a separate block at the top of the form.
- Limits form a dense grid. A single ACORD 25 can list a dozen limits across general liability, auto, umbrella, and workers compensation, each with its own label.
- Layouts vary. Agencies use different form editions, management systems, and scan quality, and many certificates arrive as flattened PDFs or phone photos.
- The important exceptions are in free text. Additional insured and waiver language often sits in DESCRIPTION OF OPERATIONS rather than in a checkbox.
Step 1: Parse the form to see what Extend reads
Start by parsing one form to check how Extend represents it. The Parse API returns layout-aware markdown organized into blocks, with checkbox states, tables, and positions on each page.
curl -X POST "https://api.extend.ai/parse" \
-H "x-extend-api-version: 2026-02-09" \
-H "Authorization: Bearer $EXTEND_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "file": { "id": "file_xK9mLPqRtN3vS8wF5hB2cQ" } }'RealDoc-Bench, Extend's open parsing benchmark, includes ACORD 125 and 126 commercial insurance applications. On one ACORD 126 page, Extend's parse returns the coverage section with each checkbox state intact:
COVERAGES
[ ] COMMERCIAL GENERAL LIABILITY
[ ] CLAIMS MADE
[x] OCCURRENCECheckbox states, table cells, and reading order survive parsing, so extraction can answer questions such as "is this policy written on an occurrence basis" from the parsed content. The default parse_performance engine is the one to use for forms: Extend recommends it for strong checkbox support, complex tables, and handwriting. On RealDoc-Bench, Extend Parse 2.0 scored 95.7% field-level accuracy on document Q&A across real-world documents.
Step 2: Define an ACORD 25 schema
Describe the fields you want as a JSON schema. Extend requires every primitive field to be nullable, every enum to include null, and supports custom date, currency, and signature types. Descriptions matter: they tell the model which box on the form each field comes from, so use the labels printed on the form.
{
"type": "object",
"properties": {
"certificate_date": {
"type": ["string", "null"],
"extend:type": "date",
"description": "The DATE (MM/DD/YYYY) in the top-right corner of the certificate."
},
"certificate_number": {
"type": ["string", "null"],
"description": "The CERTIFICATE NUMBER shown in the COVERAGES header."
},
"revision_number": {
"type": ["string", "null"],
"description": "The REVISION NUMBER shown in the COVERAGES header."
},
"producer": {
"type": "object",
"description": "The PRODUCER block: the agency or broker that issued the certificate.",
"properties": {
"name": {
"type": ["string", "null"],
"description": "Producer or agency name."
},
"address": {
"type": ["string", "null"],
"description": "Producer mailing address as one line."
},
"contact_name": {
"type": ["string", "null"],
"description": "CONTACT NAME, if filled in."
},
"phone": {
"type": ["string", "null"],
"description": "PHONE (A/C, No, Ext), if filled in."
},
"email": {
"type": ["string", "null"],
"description": "E-MAIL ADDRESS, if filled in."
}
}
},
"insured": {
"type": "object",
"description": "The INSURED block: the policyholder the certificate is about.",
"properties": {
"name": {
"type": ["string", "null"],
"description": "Named insured, exactly as printed."
},
"address": {
"type": ["string", "null"],
"description": "Insured mailing address as one line."
}
}
},
"insurers": {
"type": "array",
"description": "Every filled-in row under INSURER(S) AFFORDING COVERAGE, lettered A through F.",
"items": {
"type": "object",
"properties": {
"letter": {
"enum": ["A", "B", "C", "D", "E", "F", null],
"description": "The insurer letter, A through F."
},
"name": {
"type": ["string", "null"],
"description": "Insurer name on that row."
},
"naic_number": {
"type": ["string", "null"],
"description": "The NAIC # on that row."
}
}
}
},
"coverages": {
"type": "array",
"description": "One entry per coverage line in the COVERAGES table that has a policy number.",
"items": {
"type": "object",
"properties": {
"insurer_letter": {
"enum": ["A", "B", "C", "D", "E", "F", null],
"description": "INSR LTR for this line, linking it to an insurer."
},
"coverage_type": {
"enum": ["commercial_general_liability", "automobile_liability", "umbrella_liability", "excess_liability", "workers_compensation_and_employers_liability", "other", null],
"description": "TYPE OF INSURANCE for this line."
},
"occurrence_or_claims_made": {
"enum": ["occurrence", "claims_made", null],
"description": "Which box is checked for this line, OCCUR or CLAIMS-MADE. Null when neither is checked."
},
"additional_insured": {
"type": ["boolean", "null"],
"description": "True when ADDL INSD is marked Y for this line."
},
"subrogation_waived": {
"type": ["boolean", "null"],
"description": "True when SUBR WVD is marked Y for this line."
},
"policy_number": {
"type": ["string", "null"],
"description": "POLICY NUMBER for this line."
},
"effective_date": {
"type": ["string", "null"],
"extend:type": "date",
"description": "POLICY EFF date for this line."
},
"expiration_date": {
"type": ["string", "null"],
"extend:type": "date",
"description": "POLICY EXP date for this line."
},
"limits": {
"type": "array",
"description": "Every LIMITS entry printed for this line.",
"items": {
"type": "object",
"properties": {
"limit_name": {
"type": ["string", "null"],
"description": "The limit label as printed, for example EACH OCCURRENCE, GENERAL AGGREGATE, COMBINED SINGLE LIMIT, or E.L. EACH ACCIDENT."
},
"amount": {
"type": "object",
"extend:type": "currency",
"description": "The dollar amount for this limit.",
"properties": {
"amount": {
"type": ["number", "null"]
},
"iso_4217_currency_code": {
"type": ["string", "null"]
}
},
"required": ["amount", "iso_4217_currency_code"]
}
}
}
}
}
}
},
"description_of_operations": {
"type": ["string", "null"],
"description": "Full text of DESCRIPTION OF OPERATIONS / LOCATIONS / VEHICLES, including any additional insured or waiver wording."
},
"certificate_holder": {
"type": "object",
"description": "The CERTIFICATE HOLDER block.",
"properties": {
"name": {
"type": ["string", "null"],
"description": "Certificate holder name."
},
"address": {
"type": ["string", "null"],
"description": "Certificate holder address as one line."
}
}
},
"authorized_representative": {
"type": "object",
"extend:type": "signature",
"description": "The AUTHORIZED REPRESENTATIVE signature at the bottom of the form.",
"properties": {
"printed_name": {
"type": ["string", "null"]
},
"signature_date": {
"type": ["string", "null"],
"extend:type": "date"
},
"is_signed": {
"type": ["boolean", "null"]
},
"title_or_role": {
"type": ["string", "null"]
}
},
"required": ["printed_name", "signature_date", "is_signed", "title_or_role"]
}
}
}The Certificate of Liability Insurance template includes a production-tested ACORD 25 schema you can start from instead.
Design choices worth keeping:
- Insurers and coverages are separate arrays. This mirrors the form and keeps each coverage line's INSR LTR as a join key instead of guessing the carrier from layout.
- Limits are an array of label and amount. The set of limits differs by coverage type, so a fixed field per limit wastes schema space and breaks on unusual lines.
- Checkboxes are booleans or enums.
additional_insuredandsubrogation_waivedare booleans;occurrence_or_claims_madeis an enum withnullfor lines where neither box applies. - The signature is a signature type. It turns on Extend's signature detection, which reduces false positives on signature blocks that are not actually signed.
Step 3: Extract with citations
Upload the certificate, then send the schema to the synchronous /extract endpoint. For high volume, use the asynchronous /extract_runs endpoint with the same body.
# Upload the certificate to get a file id
curl -X POST https://api.extend.ai/files/upload \
-H "Authorization: Bearer $EXTEND_API_KEY" \
-H "x-extend-api-version: 2026-02-09" \
-F "file=@acord25.pdf"
# Extract with the schema from Step 2, saved as acord25-request.json
curl -X POST https://api.extend.ai/extract \
-H "x-extend-api-version: 2026-02-09" \
-H "Authorization: Bearer $EXTEND_API_KEY" \
-H "Content-Type: application/json" \
-d @acord25-request.jsonThe request body is the file reference plus your schema, with citations turned on:
{
"file": { "id": "file_xK9mLPqRtN3vS8wF5hB2cQ" },
"config": {
"schema": { "...": "the ACORD 25 schema from Step 2" },
"advancedOptions": { "citationsEnabled": true }
}
}Step 4: Read values, confidence, and citations
The response has two halves: output.value, shaped exactly like your schema, and output.metadata, keyed by field path, with OCR confidence and citations for each field. An illustrative, trimmed response for a general liability line looks like this:
{
"value": {
"certificate_date": "2026-10-01",
"insured": {
"name": "Harbor Point Builders LLC",
"address": "410 Pier Road, Portland, ME 04101"
},
"insurers": [
{
"letter": "A",
"name": "Example Mutual Insurance Company",
"naic_number": "12345"
}
],
"coverages": [
{
"insurer_letter": "A",
"coverage_type": "commercial_general_liability",
"occurrence_or_claims_made": "occurrence",
"additional_insured": true,
"subrogation_waived": true,
"policy_number": "CGL-2026-004417",
"effective_date": "2026-09-15",
"expiration_date": "2027-09-15",
"limits": [
{
"limit_name": "EACH OCCURRENCE",
"amount": {
"amount": 1000000,
"iso_4217_currency_code": "USD"
}
},
{
"limit_name": "GENERAL AGGREGATE",
"amount": {
"amount": 2000000,
"iso_4217_currency_code": "USD"
}
}
]
}
]
},
"metadata": {
"coverages[0].policy_number": {
"ocrConfidence": 0.99,
"citations": [
{
"page": {
"number": 1,
"width": 612,
"height": 792
},
"referenceText": "CGL-2026-004417"
}
]
}
}
}Each citation includes the page, a polygon around the source region, and the reference text, so a reviewer can open the certificate at the exact cell a policy number or limit came from. See extraction response format for every metadata field.
Step 5: Validate and route to review
Certificates are only useful if they meet your requirements. Run checks on the extracted values, and send anything uncertain or out of policy to review instead of auto-approving it:
from datetime import date
REQUIRED_LIMITS = {"EACH OCCURRENCE": 1_000_000, "GENERAL AGGREGATE": 2_000_000}
def check_coi(value, metadata, holder_name):
issues = []
insurers = {i["letter"]: i for i in value["insurers"]}
for i, line in enumerate(value["coverages"]):
if line["insurer_letter"] not in insurers:
issues.append(f"Line {i}: INSR LTR has no matching insurer")
if line["expiration_date"] and date.fromisoformat(line["expiration_date"]) < date.today():
issues.append(f"Line {i}: policy {line['policy_number']} is expired")
if line["coverage_type"] == "commercial_general_liability":
limits = {l["limit_name"]: (l["amount"] or {}).get("amount") for l in line["limits"]}
for name, minimum in REQUIRED_LIMITS.items():
if (limits.get(name) or 0) < minimum:
issues.append(f"Line {i}: {name} below {minimum:,}")
if not line["additional_insured"]:
issues.append(f"Line {i}: additional insured not marked")
if (value["certificate_holder"]["name"] or "").lower() != holder_name.lower():
issues.append("Certificate holder does not match")
low_confidence = [path for path, meta in metadata.items() if (meta.get("ocrConfidence") or 0) < 0.9]
return issues, low_confidenceSend certificates with issues or low-confidence fields to a human review step, where a reviewer sees each flagged field next to its citation and corrects it before the run continues. Extend's confidence scores guide covers how to set thresholds.
ACORD 27 schema
ACORD 27 follows the same pattern with property-specific fields: the company, loan number, perils insured, amounts of insurance and deductibles per coverage, and the additional interest, such as a mortgagee or loss payee.
{
"type": "object",
"properties": {
"evidence_date": {
"type": ["string", "null"],
"extend:type": "date",
"description": "The DATE (MM/DD/YYYY) at the top of the form."
},
"agency": {
"type": ["string", "null"],
"description": "AGENCY name and address."
},
"company": {
"type": ["string", "null"],
"description": "COMPANY: the insurer providing the coverage."
},
"policy_number": {
"type": ["string", "null"],
"description": "POLICY NUMBER."
},
"loan_number": {
"type": ["string", "null"],
"description": "LOAN NUMBER, if filled in."
},
"effective_date": {
"type": ["string", "null"],
"extend:type": "date",
"description": "EFFECTIVE DATE of the policy."
},
"expiration_date": {
"type": ["string", "null"],
"extend:type": "date",
"description": "EXPIRATION DATE of the policy."
},
"continued_until_terminated": {
"type": ["boolean", "null"],
"description": "True when the CONTINUED UNTIL TERMINATED IF CHECKED box is checked."
},
"named_insured": {
"type": ["string", "null"],
"description": "NAMED INSURED AND ADDRESS."
},
"property_location": {
"type": ["string", "null"],
"description": "Location or description of the insured property."
},
"perils_insured": {
"enum": ["basic", "broad", "special", null],
"description": "Which PERILS INSURED box is checked: BASIC, BROAD, or SPECIAL."
},
"coverages": {
"type": "array",
"description": "Each coverage line with its amount of insurance and deductible.",
"items": {
"type": "object",
"properties": {
"coverage": {
"type": ["string", "null"],
"description": "Coverage, peril, or form name as printed."
},
"amount_of_insurance": {
"type": "object",
"extend:type": "currency",
"description": "AMOUNT OF INSURANCE for this coverage.",
"properties": {
"amount": {
"type": ["number", "null"]
},
"iso_4217_currency_code": {
"type": ["string", "null"]
}
},
"required": ["amount", "iso_4217_currency_code"]
},
"deductible": {
"type": "object",
"extend:type": "currency",
"description": "DEDUCTIBLE for this coverage.",
"properties": {
"amount": {
"type": ["number", "null"]
},
"iso_4217_currency_code": {
"type": ["string", "null"]
}
},
"required": ["amount", "iso_4217_currency_code"]
}
}
}
},
"additional_interest": {
"type": "object",
"description": "The ADDITIONAL INTEREST block.",
"properties": {
"name_and_address": {
"type": ["string", "null"],
"description": "Name and address of the additional interest."
},
"interest_type": {
"enum": ["mortgagee", "lenders_loss_payable", "loss_payee", "additional_insured", null],
"description": "Which interest box is checked."
},
"loan_number": {
"type": ["string", "null"],
"description": "LOAN # in the additional interest block, if filled in."
}
}
},
"remarks": {
"type": ["string", "null"],
"description": "REMARKS text, if any."
},
"authorized_representative": {
"type": "object",
"extend:type": "signature",
"description": "The AUTHORIZED REPRESENTATIVE signature at the bottom of the form.",
"properties": {
"printed_name": {
"type": ["string", "null"]
},
"signature_date": {
"type": ["string", "null"],
"extend:type": "date"
},
"is_signed": {
"type": ["boolean", "null"]
},
"title_or_role": {
"type": ["string", "null"]
}
},
"required": ["printed_name", "signature_date", "is_signed", "title_or_role"]
}
}
}Field labels differ slightly between ACORD editions. Keep descriptions tied to the labels printed on the forms you receive, and test against samples of each edition in your intake.
Applications and packets: ACORD 125, 126, and 140
Commercial applications run several pages and bundle section forms, such as ACORD 126 for general liability and ACORD 140 for property, behind the ACORD 125. Two adjustments help:
- Split the packet first. Extend's Split API separates a submission into typed sub-documents, such as the ACORD 125, each section form, loss runs, and schedules, and returns each as its own file for extraction with the right schema. See document splitting APIs for insurance, legal, and healthcare for packet strategies.
- Classify before you extract. The ACORD Form Classifier template runs Parse, then Classify, then Extract on a combined ACORD 125, 126, and 140 application, so only confirmed ACORD forms reach the 55-field extraction schema.
- Use arrays for repeating sections. Model locations, vehicles, and prior carriers as arrays of objects, and choose the
extraction_operatorprocessor for long, table-heavy documents with large arrays.
Filling ACORD forms with the Edit API
The same forms often need to be filled out, not just read. ACORD forms are commonly distributed as fillable PDFs, and Extend's Edit API detects and fills PDF form fields from data or natural-language instructions, then returns the completed PDF. Flattening makes the filled values non-editable before you send the form on.
Catching altered certificates with Extend Verify
A certificate can be edited before it reaches you: a changed expiration date, a raised limit, or an added certificate holder. Extend Verify checks a PDF or image for signs of manipulation, including AI edits as small as a single character, patched text and overlays, and changes made after a document was signed. It returns a risk level with evidence located on the page, so a reviewer can see exactly what changed before your team relies on the extracted coverage. For how Verify compares with other tools, see the document fraud detection API comparison.
FAQ
How do I extract data from an ACORD 25 certificate of insurance?
Upload the certificate to Extend, then call the Extract API with a JSON schema that describes the fields you need: producer, insured, insurers A through F with NAIC numbers, each coverage line with its policy number, dates, checkboxes, and limits, the certificate holder, and the authorized representative's signature. Turn on citations so every value comes back with the page and region it was read from, then validate dates and limits and route low-confidence fields to review.
Is there a ready-made template for extracting ACORD forms?
Yes. Extend publishes a Certificate of Liability Insurance Extractor for ACORD 25 certificates and an ACORD Form Classifier for commercial insurance applications such as ACORD 125, 126, and 140. Each template page runs the pipeline on a real document, lets you run your own document for free, and includes example code in TypeScript, Python, Java, and Go plus the workflow JSON.
Can Extend read the checkboxes on ACORD forms?
Yes. Extend's Parse API marks each checkbox as checked or unchecked, for example [x] OCCURRENCE and [ ] CLAIMS MADE on an ACORD 126 page from RealDoc-Bench, and the default parse_performance engine is built for strong checkbox support. In an extraction schema, model checkboxes as booleans or as enums such as occurrence or claims_made.
How do I link each coverage line to the right insurer on an ACORD 25?
Extract the INSURER(S) AFFORDING COVERAGE rows as an array with letter, name, and NAIC number, and extract INSR LTR on each coverage line as an enum of A through F. Joining the two in code gives each policy its insurer and NAIC number.
What is the difference between ACORD 25 and ACORD 27?
ACORD 25 is the Certificate of Liability Insurance: it summarizes general liability, auto, umbrella, and workers compensation coverage for a certificate holder. ACORD 27 is the Evidence of Property Insurance: it shows property coverage, amounts of insurance, deductibles, and an additional interest such as a mortgagee or loss payee, and is common in lending and real estate.
Can Extend process scanned or photographed ACORD forms?
Yes. Extend parses PDFs and images, including scans, and returns OCR confidence with each extracted field when citations are enabled. Route fields below your confidence threshold to a human review step before they reach a policy or compliance system.
Can Extend fill out ACORD forms as well as read them?
Yes. ACORD forms are often fillable PDFs, and Extend's Edit API detects and fills PDF form fields from data or natural-language instructions, then returns a completed PDF that can be flattened so the values can't be changed.
How do I detect altered or fake certificates of insurance?
Extend Verify checks a PDF or image for signs of manipulation, including AI edits, patched text, overlays, and changes made after a document was signed, and returns a risk level with evidence located on the page. Run it on incoming certificates before relying on the extracted coverage.
