Compliance & Vendor Risk

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.

See exact current pricing tiers on RapidAPI →

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

Try ACORD 25 Data Extraction & COI Verification on RapidAPI