ACORD 25 Data Extraction & COI Verification
Verify a certificate against your requirements — and get told plainly when a value could not be read.
The problem
General contractors, property managers, and franchisors deal with the same recurring task: a subcontractor, vendor, or tenant submits a Certificate of Insurance (typically an ACORD 25), and someone has to check it against the actual contract requirements — minimum liability limits, additional-insured status, waiver of subrogation, valid policy dates. Done by hand it is slow, repetitive work across a differently-formatted PDF from a different agent every time, and it's easy to miss something. A missed additional-insured endorsement or an expired policy can mean an uninsured loss the GC or landlord ends up covering.
Who it's for
Risk and compliance teams at general contractors, property management companies, franchisors, and insurance/vendor-risk platforms that need to check certificates of insurance against coverage requirements repeatedly, per vendor or per property.
Use cases
- Verifying a new subcontractor's certificate against a contract's minimum coverage requirements before granting site access
- Re-checking a vendor's certificate at renewal to confirm coverage hasn't lapsed
- Embedding automated COI checks into a vendor-management or procurement platform
- Extracting structured data from a certificate without running a compliance check, for your own downstream logic
What it does
Send a certificate as a file URL or base64-encoded image/PDF, and get back structured extraction of the standard ACORD 25 fields — named insured, insurers, each coverage section's limits and dates, additional-insured and subrogation-waived flags. Send your own coverage requirements in the same call and it also returns a compliant, deficient, or cannot_determine verdict with an itemized list of exactly what's missing, not just a pass/fail.
Why Aervik's implementation is different
When a value cannot be read, this API declines to report it rather than guessing at it. A smudged, cut-off, or placeholder cell comes back in a `withheld` list with a status of `unreadable` and a plain sentence saying the value was present on the certificate but could not be read — and if your requirements depended on that field, the verdict is `cannot_determine` rather than a pass or a fail. No number, date, or `true` is returned unless it was quoted from the document. A blank cell and an illegible one are different answers, and a gap in the data is reported differently from a gap in coverage. Policy dates are the one place a blank and an illegible cell cannot be told apart, so a policy date missing from a coverage row the certificate shows is withheld as not established from the certificate, never reported as absent. Underneath that, extraction and the compliance verdict are deliberately separate steps: a vision model reads the document, and the verdict is a fixed deterministic rules engine over the extracted fields, never an AI judgment call — so the same requirements and the same data always produce the same verdict. **PDF certificates:** `file_url` carries an **image** of the certificate (jpeg, png, webp, gif). Send a PDF as `base64_image` with `"mime_type": "application/pdf"` — that path is fully supported, and a PDF URL is rejected up front rather than failing later.
Example
Request
POST /verify
{
"file_url": "https://aerviklabs.com/research/coi-honesty-test/trap4-illegible-expiration.png",
"requirements": {
"general_liability": { "each_occurrence_min": 1000000, "personal_adv_injury_min": 1000000 },
"min_days_until_expiration": 30
}
}
Response
Captured live 2026-09-18 from the request above, values unedited. The verdict is deterministic; the extraction is a model read of that certificate.
{
"compliant": false,
"verdict": "cannot_determine",
"deficiencies": [
{
"field": "general_liability.personal_adv_injury",
"required": "Personal & Advertising Injury limit >= $1,000,000",
"actual": "not found on certificate",
"status": "absent",
"reason": "Personal & Advertising Injury limit could not be determined from the certificate."
},
{
"field": "general_liability.policy_expiration_date",
"required": "Policy expiration date on file, valid at least 30 more day(s)",
"actual": "present on the certificate but could not be read",
"status": "unreadable",
"reason": "General Liability policy expiration date could not be determined. The value was withheld because it could not be quoted from the document -- this is a request for a clearer copy, not a coverage deficiency."
}
],
"evaluated": [
"general_liability",
"min_days_until_expiration"
],
"withheld": [
"general_liability.policy_expiration_date"
],
"parsed": {
"named_insured": "Harborline Mechanical LLC",
"producer": "Kestrel Risk Partners",
"insurers": [
{
"letter": "A",
"name": "Cascade Mutual Casualty Company",
"naic_number": "24678"
},
{
"letter": "B",
"name": "Puget Indemnity Insurance Co.",
"naic_number": "31955"
}
],
"certificate_holder": "Vance Property Group",
"certificate_date": "2026-03-14",
"description_of_operations": "Mechanical contracting services. Certificate holder is included as additional insured with respect to General Liability per written contract.",
"general_liability": {
"insurer_letter": "A",
"policy_number": "CGL-4471902",
"policy_effective_date": "2025-04-01",
"policy_expiration_date": null,
"additional_insured": true,
"subrogation_waived": true,
"each_occurrence": 1000000,
"damage_to_rented_premises": 100000,
"med_exp": 15000,
"personal_adv_injury": null,
"general_aggregate": 2000000,
"products_comp_op_agg": 2000000,
"claims_made": false
},
"auto_liability": {
"insurer_letter": "B",
"policy_number": "BAP-9920117",
"policy_effective_date": "2025-04-01",
"policy_expiration_date": "2027-04-01",
"additional_insured": true,
"subrogation_waived": false,
"combined_single_limit": 1000000,
"bodily_injury_per_person": 500000,
"bodily_injury_per_accident": null,
"property_damage_per_accident": null,
"any_auto": true
},
"umbrella_excess": null,
"workers_comp": null,
"other_coverages": [],
"confidence": 0.95
}
}
Calling it directly — real, working requests you can paste and run:
cURL
curl -X POST "https://certificate-of-insurance-verification-compliance-check-api.p.rapidapi.com/verify" \
-H "content-type: application/json" \
-H "X-RapidAPI-Key: $RAPIDAPI_KEY" \
-H "X-RapidAPI-Host: certificate-of-insurance-verification-compliance-check-api.p.rapidapi.com" \
-d '{"file_url": "https://aerviklabs.com/research/coi-honesty-test/trap4-illegible-expiration.png", "requirements": {"general_liability": {"each_occurrence_min": 1000000, "personal_adv_injury_min": 1000000}, "min_days_until_expiration": 30}}'
Python
import os
import requests
RAPIDAPI_HOST = "certificate-of-insurance-verification-compliance-check-api.p.rapidapi.com"
resp = requests.post(
f"https://{RAPIDAPI_HOST}/verify",
json={
"file_url": "https://aerviklabs.com/research/coi-honesty-test/trap4-illegible-expiration.png",
"requirements": {
"general_liability": {
"each_occurrence_min": 1000000,
"personal_adv_injury_min": 1000000,
},
"min_days_until_expiration": 30,
},
},
headers={
"X-RapidAPI-Key": os.environ["RAPIDAPI_KEY"],
"X-RapidAPI-Host": RAPIDAPI_HOST,
},
)
result = resp.json()
print(result["verdict"], result["deficiencies"], result["withheld"])
Node.js
const RAPIDAPI_HOST = 'certificate-of-insurance-verification-compliance-check-api.p.rapidapi.com';
const res = await fetch(`https://${RAPIDAPI_HOST}/verify`, {
method: 'POST',
headers: {
'content-type': 'application/json',
'X-RapidAPI-Key': process.env.RAPIDAPI_KEY,
'X-RapidAPI-Host': RAPIDAPI_HOST,
},
body: JSON.stringify({
file_url: 'https://aerviklabs.com/research/coi-honesty-test/trap4-illegible-expiration.png',
requirements: {
general_liability: { each_occurrence_min: 1000000, personal_adv_injury_min: 1000000 },
min_days_until_expiration: 30,
},
}),
});
const result = await res.json();
console.log(result.verdict, result.deficiencies, result.withheld);
Machine-readable: the OpenAPI 3.1 document for this API, generated from its code — import it into Postman or a client generator. More on the developer page.
A ready-made Postman collection is also available: import it and the shared environment in Postman (Import, then select the file), then set RAPIDAPI_KEY to your RapidAPI key.
Key features
- Structured extraction of every standard ACORD 25 coverage section
- Three verdicts, not two: compliant, deficient, or cannot_determine when a field your requirements depend on could not be read
- A withheld list naming every value we declined to report, so a gap in the data reads differently from a gap in coverage
- An evaluated list naming exactly which of your requirement keys were checked, so a clean result proves what was actually examined rather than what was skipped
- Per-field status — present, absent, or unreadable — because a blank cell and an illegible one are different answers; a policy date missing from a coverage row is never reported absent
- No number, date, or true is returned unless it was quoted from the document; unquotable values are nulled in code, not asked for in a prompt
- Itemized deficiencies, not a bare pass/fail — exactly which requirement failed and why
- Deterministic rules engine for the verdict — never an AI guess on pass/fail
Pricing
Free to start: 50 requests a month, hard-capped, with no card required — enough to run a real evaluation including the requirement-tuning loop. Every call to any endpoint counts as one request, whatever it returns: a verification that comes back unreadable or cannot_determine uses one, the same as a clean pass, and so do /parse and saving a requirements profile. Paid monthly plans begin at $49 for 150 requests and scale to 3,000, with overage priced per additional request beyond the top plan — sized for occasional checks up to a high-volume vendor-onboarding workflow.
Frequently asked questions
What document formats does this API accept?
A publicly fetchable file_url or a base64-encoded file. Accepted mime types are image/jpeg, image/png, image/webp, image/gif, and application/pdf — PDF support matters here since ACORD certificates are commonly issued as PDFs.
What's the difference between /parse and /verify?
POST /parse returns structured extraction only — named insured, insurers, every coverage section's limits and dates. POST /verify does the same extraction and also checks it against coverage requirements you supply (or a saved profile), returning a compliant, deficient, or cannot_determine verdict with an itemized list of exactly what's missing.
How is the verdict determined?
By a deterministic rules engine over the already-extracted fields, never an AI judgment call — so the same requirements and the same extracted data always produce the same verdict.
What does the confidence score mean?
The model's own assessment of how well it read the whole document, from 0 to 1. It is not a refusal: a value that could not be read comes back null — with status unreadable, or named in withheld — even when the confidence is high. Read those fields first, and treat a low confidence as a reason to look at the original document.
Can I save my coverage requirements and reuse them?
Yes. Save a named set once with POST /requirements and pass its id on later POST /verify calls, or send the requirements inline on every call — both work. Saved profiles are stored in a database, so an id you receive stays resolvable; read one back with GET /requirements/{id}.
What happens if a coverage section isn't shown on the certificate?
That section is returned as null — the extractor is instructed to never invent a coverage section that isn't actually on the certificate.
Does it check that the named insured is the company I contracted with?
No. The named insured is extracted and returned, but no requirement compares it with a name you supply, so a compliant verdict says nothing about it. Compare it yourself.
Does it detect a fake or altered certificate?
No. It reads what the certificate prints. A forged or edited certificate that prints passing values comes back compliant. To confirm coverage exists, ask the issuing agent or insurer.
Is a 'compliant' verdict a guarantee the policy is valid?
No. This API is advisory only, not a substitute for professional review. A compliant result is not a guarantee the underlying policy is valid, in force, or will respond to a real claim.
Saved requirement profiles
Send your coverage requirements inline on any /verify call, or save a named set once with
POST /requirements and reference it by id on later calls. Both paths work and are backed by
a real database, so an id you receive stays resolvable.
How it works — and what it doesn’t do
You set the rules; we do the matching. The API compares a certificate of insurance against the coverage requirements you send it — your minimum limits, additional-insured status, waiver of subrogation, policy dates — and reports where they don’t line up. It never suggests what your requirements should be: no recommended limits, no templated requirement sets, no opinion on whether coverage is adequate. That’s a design boundary, not a roadmap gap. Results are a matching output, not a substitute for review by a qualified insurance professional.
A “compliant” result
is not a guarantee that the underlying policy is valid, in force, or will respond to a
real claim. Read withheld and each deficiency’s status first — a value that could not be read
comes back null even when the confidence score is high — and treat a low confidence as a reason to look
at the original document. Nothing here should be read as legal,
insurance, or risk-management advice. See the API’s own /terms endpoint for the full
disclosure.
Further reading
- Free Certificate of Insurance Checker — check one certificate against your requirements, no sign-up
- What a Certificate of Insurance Actually Proves — what the document can and cannot tell you
- Certificate Holder vs Additional Insured
- What Makes a Certificate of Insurance Deficient
- When a Certificate Can’t Be Read — what this API returns for an illegible value
- Blank, Unreadable, or Absent — the same problem from the side of a developer building extraction
- Coverage Requirements as Data — the requirements schema, and five ways a check silently passes a rule it never ran
- Automating ACORD 25 Verification for Vendor Onboarding
- The COI Extraction Honesty Test — four synthetic certificates with documented ground truth, and our own results, including the one we failed
- Why manual certificate-of-insurance review misses things