ACORD 25 COI Checker: Certificate of Insurance Compliance
Pricing
from $80.00 / 1,000 certificate checkeds
ACORD 25 COI Checker: Certificate of Insurance Compliance
ACORD 25 certificate of insurance (COI) checker: reads COI PDFs (fillable, flattened or scanned) into JSON and checks limits, expiry, additional insured, waiver of subrogation, primary/non-contributory and holder. PASS/FAIL/REVIEW with reasons and per-field confidence. Rules-based, no AI.
Pricing
from $80.00 / 1,000 certificate checkeds
Rating
0.0
(0)
Developer
Haniel Azevedo
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
5 days ago
Last modified
Categories
Security
Limited permissionsShare
Turn ACORD 25 certificate of insurance PDFs into structured JSON and a PASS / FAIL / REVIEW verdict against your insurance requirements. It is built for developers and AI agents: you call it over the API, it reads fillable, flattened and scanned certificates, and it returns every field with a confidence score.
- Extracts the producer, insured, insurers A–F with NAIC numbers, every coverage line (insurer letter, policy number, effective/expiration dates, limits, ADDL INSD / SUBR WVD), the description of operations, ACORD 101 additional remarks, the certificate holder and the form edition (2010/05, 2014/01, 2016/03).
- Checks limits (GL, auto CSL, umbrella, WC/EL), expiry (with "must not expire within N days"), additional insured, waiver of subrogation, primary & non-contributory wording and certificate holder name.
- Rules only, no AI/LLM, so the same PDF always gives the same answer. Your certificates are never sent to a third-party model.
- Pay per certificate checked. Files that fail, blank forms, scans with OCR off and the sample certificate are free.
Who it is for
- Developers building vendor / subcontractor onboarding, property management, construction compliance or procurement tools who need COI data in JSON and do not want to maintain a PDF parser.
- AI agents (Claude, ChatGPT, Cursor and others through the Apify MCP server) that receive a COI and need a reliable yes/no with reasons rather than a guess from a vision model.
- Risk / compliance teams who want a CSV of every certificate with its verdict.
Example input
The Console form is prefilled with a real public certificate (Cornell University's sample ACORD 25) and a typical requirement set. Click Start without changing anything:
{"pdfUrls": ["https://scl.cornell.edu/sites/scl/files/documents/OSFL-COI-Correct%20Sample%20Updated.pdf"],"requirements": {"glEachOccurrenceMin": 1000000,"glGeneralAggregateMin": 2000000,"autoCombinedSingleLimitMin": 1000000,"additionalInsuredRequired": true,"primaryNonContributoryRequired": true,"certificateHolderName": "Cornell University","minDaysUntilExpiry": 30,"asOfDate": "2021-01-15"}}
Example output
That run takes about 9 seconds and returns one certificate with verdict PASS: GL $1M / $2M with additional insured, auto CSL $1M, holder name matches, and nothing expires within 30 days of the asOfDate. The sample certificate is free. Trimmed result (run of 1 Nov 2026):
{"fileName": "OSFL-COI-Correct Sample Updated.pdf","verdict": "PASS","reasonCodes": [],"asOfDate": "2021-01-15","insuredName": "SAMPLE VENDOR","certificateHolderName": "CORNELL UNIVERSITY, It's","earliestExpirationDate": "2021-10-01","overallConfidence": 0.95,"certificate": {"inputType": "flattened","formEdition": "2014/01","producer": {"name": "SAMPLE BROKER"},"coverages": {"generalLiability": {"insurerLetter": "A","policyNumber": "CGL 123456","effectiveDate": "2020-10-01","expirationDate": "2021-10-01","additionalInsured": true,"limits": {"eachOccurrence": 1000000,"generalAggregate": 2000000,"productsCompletedOpsAggregate": 1000000}},"autoLiability": {"insurerLetter": "A","policyNumber": "CAL 987654","expirationDate": "2021-10-01","limits": {"combinedSingleLimit": 1000000}}}},"demo": true,"charged": false}
Quick start (API)
Python (pip install apify-client)
from apify_client import ApifyClientclient = ApifyClient("<YOUR_APIFY_TOKEN>")run = client.actor("hanielazevedo/acord-25-coi-checker").call(run_input={"pdfUrls": ["https://scl.cornell.edu/sites/scl/files/documents/OSFL-COI-Correct%20Sample%20Updated.pdf"],"requirements": {"glEachOccurrenceMin": 1000000,"glGeneralAggregateMin": 2000000,"autoCombinedSingleLimitMin": 1000000,"wcElEachAccidentMin": 1000000,"additionalInsuredRequired": True,"waiverOfSubrogationRequired": True,"primaryNonContributoryRequired": True,"certificateHolderName": "Riverside Property Management LLC","minDaysUntilExpiry": 30}})for cert in client.dataset(run["defaultDatasetId"]).iterate_items():if cert["recordType"] == "certificate":print(cert["fileName"], cert["verdict"], cert["reasonCodes"])else:print(cert["fileName"], "ERROR", cert["errorCode"], cert["errorMessage"])
JavaScript / TypeScript (npm i apify-client)
import { ApifyClient } from 'apify-client';const client = new ApifyClient({ token: process.env.APIFY_TOKEN });const run = await client.actor('hanielazevedo/acord-25-coi-checker').call({pdfUrls: ['https://scl.cornell.edu/sites/scl/files/documents/OSFL-COI-Correct%20Sample%20Updated.pdf'],requirements: { glEachOccurrenceMin: 1000000, additionalInsuredRequired: true, minDaysUntilExpiry: 30 },});const { items } = await client.dataset(run.defaultDatasetId).listItems();for (const c of items) console.log(c.fileName, c.verdict ?? c.errorCode, c.reasonCodes ?? c.errorMessage);
Plain HTTP (one call, returns the dataset)
curl -X POST "https://api.apify.com/v2/acts/hanielazevedo~acord-25-coi-checker/run-sync-get-dataset-items" \-H "Authorization: Bearer $APIFY_TOKEN" -H "Content-Type: application/json" \-d '{"pdfUrls":["https://scl.cornell.edu/sites/scl/files/documents/OSFL-COI-Correct%20Sample%20Updated.pdf"],"requirements":{"glEachOccurrenceMin":1000000}}'
AI agents (MCP)
Add the Apify MCP server with this Actor as a tool:
{"mcpServers": {"apify": {"url": "https://mcp.apify.com/?tools=hanielazevedo/acord-25-coi-checker","headers": { "Authorization": "Bearer <YOUR_APIFY_TOKEN>" }}}}
The agent can then call the tool with pdfUrls and requirements and read verdict, reasons[].message and lowConfidenceFields. Each reason has a human-readable message the agent can quote back to the user.
PDFs that are not public
The Actor runs with limited permissions, so it can only read storage you hand to it. Upload the PDFs to an Apify key-value store (Console → Storage → Key-value stores, or client.key_value_store(id).set_record("acme.pdf", data, "application/pdf")), then pass "pdfStore": "<store id or name>". You can add "pdfKeys": ["acme.pdf"] to process only some records. Selecting the store grants read-only access to that one store, for that run only.
Step by step (Console)
- Open the Actor and click Try for free. The input is prefilled with a public sample certificate from Cornell University, which is processed for free.
- Paste your PDF links into Certificate PDF URLs, or pick a key-value store that holds your PDFs.
- Optionally, fill Requirements with the JSON for the limits and endorsements you need (see below).
- Click Start. Each certificate becomes one row in the Output tab, and
SUMMARY.csvin the key-value store holds one line per certificate for Excel or Google Sheets.
Input
| Field | Type | Description |
|---|---|---|
pdfUrls | string[] | Public http(s) links to COI PDFs. One PDF may hold several certificates and ACORD 101 pages. |
pdfStore | string | Key-value store with PDFs (read-only access, granted for this run only). |
pdfKeys | string[] | Optional record keys inside pdfStore. Defaults to every *.pdf. |
requirements | object | Optional checks, all keys optional (below). |
enableOcr | boolean | Read scanned certificates with open-source Tesseract OCR (default true). |
minFieldConfidence | "0.6"–"0.9" | Fields below this are listed in lowConfidenceFields, and if one is used in a decision the verdict becomes REVIEW. Default 0.8. |
maxCertificates | integer | Safety cap per run (default 200). |
maxFileSizeMb | integer | Larger files are rejected free of charge (default 20). |
requirements keys
| Key | Meaning |
|---|---|
glEachOccurrenceMin, glGeneralAggregateMin, glProductsCompletedOpsMin | General liability minimums (USD) |
autoCombinedSingleLimitMin | Auto liability combined single limit |
umbrellaEachOccurrenceMin, umbrellaAggregateMin | Umbrella / excess minimums |
wcElEachAccidentMin, wcElDiseaseEachEmployeeMin, wcElDiseasePolicyLimitMin | Employers' liability minimums |
allowUmbrellaToMeetLimits | Default true. A GL or auto shortfall is covered by the umbrella (reason UMBRELLA_USED_TO_MEET_LIMIT, INFO). |
requireGeneralLiability, requireAutoLiability, requireUmbrella, requireWorkersCompensation | The coverage must be listed |
additionalInsuredRequired + additionalInsuredOn | ADDL INSD must be marked on these coverages (default ["generalLiability"]) |
waiverOfSubrogationRequired + waiverOfSubrogationOn | SUBR WVD must be marked (default ["generalLiability","workersCompensation"]) |
primaryNonContributoryRequired | "Primary and non-contributory" must appear in the description / remarks |
certificateHolderName, insuredName | Fuzzy name match that tolerates case, punctuation, "Inc./LLC" and OCR slips |
minDaysUntilExpiry, asOfDate | Policies must not expire within N days of asOfDate (YYYY-MM-DD, default today UTC) |
Any coverage that is not listed in your requirements is still date-checked, but problems with it are reported as INFO and do not change the verdict. Unknown keys fail the run immediately, so a typo can't silently pass everything.
Output
There is one dataset item per certificate (recordType: "certificate") or per failed file (recordType: "error"). This example is shortened:
{"recordType": "certificate","fileName": "acme-roofing-2026.pdf","verdict": "FAIL","reasonCodes": ["GL_EACH_OCCURRENCE_BELOW_MIN", "SUBR_WAIVER_MISSING"],"reasons": [{"code": "GL_EACH_OCCURRENCE_BELOW_MIN", "severity": "FAIL","message": "General liability eachOccurrence is $1,000,000, required $2,000,000.","fields": ["coverages.generalLiability.limits.eachOccurrence"], "actual": 1000000, "required": 2000000},{"code": "SUBR_WAIVER_MISSING", "severity": "FAIL","message": "General liability: waiver of subrogation is not indicated.","fields": ["coverages.generalLiability.subrogationWaived"]},{"code": "EXPIRES_WITHIN_N_DAYS", "severity": "INFO","message": "Auto liability expires on 2020-10-12, 41 days after 2020-09-01 (minimum 60)."}],"asOfDate": "2020-09-01","insuredName": "ABC Company 12345","certificateHolderName": "CITY OF TEMECULA","earliestExpirationDate": "2020-10-12","certificate": {"pageNumber": 1, "inputType": "flattened", "formEdition": "2016/03", "ocrUsed": false,"certificateDate": "2020-08-24", "certificateNumber": "1907915241","producer": {"name": "Clifford & Bradford Insurance Agency", "phone": "...", "email": "..."},"insured": {"name": "ABC Company 12345", "address": "..."},"insurers": {"A": {"name": "United Financial Casualty Co.", "naic": "11770"}, "B": {"name": "West American Ins Co.", "naic": "44393"}},"coverages": {"generalLiability": {"present": true, "insurerLetter": "B", "policyNumber": "GL-123456","effectiveDate": "2019-12-12", "expirationDate": "2020-12-12", "additionalInsured": true, "subrogationWaived": false,"commercialGeneralLiability": true, "occurrence": true, "claimsMade": false, "aggregateAppliesPer": null,"limits": {"eachOccurrence": 1000000, "damageToRentedPremises": 500000, "medicalExpense": 5000,"personalAdvertisingInjury": 1000000, "generalAggregate": 2000000, "productsCompletedOpsAggregate": 2000000}},"autoLiability": {"present": true, "insurerLetter": "A", "policyNumber": "AUTO7891011", "anyAuto": false,"scheduledAutos": true, "hiredAutosOnly": true, "nonOwnedAutosOnly": true, "limits": {"combinedSingleLimit": 1000000}},"umbrellaLiability": {"present": false},"workersCompensation": {"present": true, "insurerLetter": "C", "policyNumber": "WC12131415", "perStatute": false,"limits": {"elEachAccident": 1000000, "elDiseaseEachEmployee": 1000000, "elDiseasePolicyLimit": 1000000}},"otherCoverages": []},"descriptionOfOperations": "...", "additionalRemarks": [],"certificateHolder": {"name": "CITY OF TEMECULA", "address": "..."}},"fieldConfidence": {"coverages.generalLiability.limits.eachOccurrence": 0.95, "...": 0.95},"lowConfidenceFields": [],"overallConfidence": 0.95,"charged": true}
- Verdicts.
PASSmeans every requirement was met.FAILmeans at least one requirement was clearly not met.REVIEWmeans a human should look: a field used in a decision is unreadable or low-confidence, an endorsement appears only in the description, or the policy is not yet effective. - Confidence. Fillable PDF widgets score 0.99 and the text layer 0.95. Scanned (OCR) fields are lower and scale with how well the page aligned to the ACORD template.
SUMMARY.csvsits in the run's default key-value store, with its link inOUTPUT.summaryCsvUrl. It has one row per certificate or error, including the verdict, reason codes, main policy numbers, limits and expiration dates.OUTPUTholds the run totals: files, certificates, pass / fail / review and charged.
Error codes (never charged)
INVALID_INPUT, INVALID_URL, BLOCKED_URL, DNS_ERROR, DOWNLOAD_FAILED, DOWNLOAD_TIMEOUT, ACCESS_DENIED, NOT_FOUND, HTTP_ERROR, FILE_TOO_LARGE, EMPTY_FILE, UNSUPPORTED_FORMAT, CORRUPT_FILE, ENCRYPTED_PDF, SCANNED_NOT_SUPPORTED, OCR_FAILED, NOT_ACORD25, NO_DATA_FOUND, PARSE_FAILED, BUDGET_EXCEEDED. Each comes with a retryable flag.
Accuracy
These results are field-level, measured on a test set with hand-checked ground truth. The parser was frozen before the held-out set was scored, and the held-out set was scored once.
| Set | Certificates | Fields | Accuracy |
|---|---|---|---|
| Development | 47 | 2,540 | 99.2 % |
| Held-out (never used for tuning) | 46 | 2,562 | 98.6 % |
| Held-out: real public sample COIs | 6 | 227 | 97.8 % |
| Held-out: fillable / flattened / shifted PDFs | 100 % / 99.5 % / 99.7 % | ||
| Held-out: scanned (OCR) PDFs | 95.1 % |
The sets mix real sample certificates published by universities and public agencies with synthetic ACORD 25 PDFs (2010 and 2016 layouts) generated with known values. The synthetic PDFs come in fillable, flattened, page-shifted and 300-dpi scanned variants with noise and skew.
Pricing
Pricing is pay per event: $0.08 per cert-checked event, one per certificate that was parsed successfully. A PDF with 3 certificate pages counts as 3 events. ACORD 101 remark pages, failed files, blank forms, scans with OCR turned off and the sample certificate are free. The run stops charging when it reaches your maximum total charge, and the remaining certificates are reported as BUDGET_EXCEEDED without being charged.
FAQ
Does it use AI? No. It aligns each page to the ACORD 25 template geometry and reads every box with deterministic rules. OCR uses the open-source Tesseract engine and runs inside the Actor, so no document leaves Apify.
Which forms are supported? ACORD 25 Certificate of Liability Insurance in the 2010/05, 2014/01 and 2016/03 editions, as fillable, flattened (printed to PDF) or scanned files. ACORD 101 Additional Remarks pages are attached to the preceding certificate as additionalRemarks. Other ACORD forms (24, 27, 28, etc.) are returned as NOT_ACORD25 and are free.
How is "additional insured" decided? The ADDL INSD box on each required coverage line comes first. If the box is empty but the description says the holder is an additional insured, the verdict is REVIEW (ADDL_INSD_ONLY_IN_DESCRIPTION), because many contracts require the box.
Can it check that the policy is real? No. A COI is informational only. This Actor checks what the certificate says, not whether the insurer would honor it. Verify with the producer or carrier for high-risk work.
Can I send images (JPG/PNG)? Not yet. Convert them to PDF first.
Is my data stored? Results stay in your own Apify storage. The Actor runs with limited permissions and can only read the store you select.
Limitations
- Scanned certificates are slower (about 5–15 s per page) and less accurate (about 95 % of fields). Long names can be truncated, and a few characters may be misread. Most scans should be expected to return REVIEW.
- Very long insurer names that run into the NAIC column on flattened PDFs may be cut off. For example, "…Company of Pittsburgh, PA" can come back as "…of Pittsburg".
- Two "other coverage" rows with the same insurer row height (e.g., Professional and Cyber) can be merged into one
otherCoveragesentry. - ADDL/SUBR marks that sit far below their coverage line in a hand-edited form can be missed.
- Handwritten certificates, photos taken at an angle, rotated pages and non-English forms are not supported.
- The verdict is a screening aid, not legal or insurance advice.
Other Actors by Haniel
- Insurance Loss Run Extractor: Loss Run PDF to Excel & CSV: Extract claims and policy-year totals from carrier loss run PDFs into Excel, CSV and JSON, each row checked against the report totals. $0.04 per validated claim.
- Spec Book to Submittal Register (CSI MasterFormat, Procore): Turn a construction project manual (spec book PDF) into a submittal register and a Procore import CSV, by CSI MasterFormat section. $0.05 per spec page.
- Rent Roll to Excel: Yardi, AppFolio & RealPage Normalizer: Normalize Yardi, AppFolio, RealPage, Entrata and Buildium rent rolls (XLSX, CSV, PDF) into one validated Excel with occupancy and loss-to-lease. $0.02 per unit.
- PDF to Markdown Converter & PDF Text Extractor (OCR, DOCX): PDF, scanned PDF, DOCX, HTML and images to clean Markdown or text for RAG and AI agents. $3 per 1,000 documents.