Certificate of insurance automation
Two Python tools replaced the by-hand handling of an insurance agency's certificates, splitting 650 batched PDFs into per-holder files and rolling the dates forward on about 1,300 live certificates in place, every one passing an independent QA check.
- Date
- Stack
- Python, PyMuPDF, PyPDF2, Pillow
The finding
A small insurance business renews a few hundred ACORD 25 certificates of insurance every year. The work used to be manual twice over: pulling individual certificates out of batch files, then re-issuing each one with new dates. Both steps are now scripts. The second one comes with an independent checker, because an insurance document with a wrong date is worse than one that was never touched.
Context
The certificates arrived as batch PDFs of 50 certificates each, numbered up through 650, and the agency needed one file per certificate holder. Then, at renewal, every live certificate, about 1,300 files in all, needed its issue date and policy dates moved forward, and the result had to look identical to the original apart from those dates. I did the first job in December 2025 and the second one in September 2026.
What was built
The splitter. split_pdf.py, a short PyPDF2 script. Each certificate is one page, so
it walks the batch page by page, extracts the text, finds the line carrying the holder label,
and writes that page out as its own PDF named after the holder, with path characters
scrubbed from the name. Pages with no label are reported and skipped rather than saved under
a guess. It is deliberately small: two configuration lines at the top and one loop.
The date rollover. A small library and a set of command-line tools:
coi_dates.pyfinds the dates on a certificate, plans which ones change, and rewrites them. Detecting, planning and applying are separate steps, so a batch run can report what it would change before writing anything.batch_roll_dates.pyruns the update over a folder and writes to a mirror folder. Originals are only ever read. Every output is read back, pixel-diffed against its original outside the date boxes, and checked for character spacing drift. If anything looks wrong the original is copied instead and the file is flagged.qa_check.pyis a second pass written to disagree with the first. It restates the rules on its own, reads the PDFs a different way (page words instead of raw spans), and runs its own pixel diff, so a bug in the update cannot vouch for itself. It checks that each original has exactly one output, that files open, that untouched files are byte-identical, that every date is either correctly changed or unchanged, that no other text or pixel moved, and that no fonts were added.overlap_report.pyfinds holders who already have a certificate elsewhere in the set, and files that are exact duplicates, so the agency could trim before sending.
The certificates came in two shapes that needed different handling. Text PDFs have real text spans, so the new date is set in the certificate’s own embedded font at each original character’s position; no new font is embedded and the file size does not change. PDFs made through Microsoft Print To PDF have no text layer at all. Every digit is a vector outline. For those, glyph-sized paths are grouped into lines, rendered, and matched against reference Helvetica digit bitmaps, and only the digits that change are redrawn.
Evidence
The rules live at the top of the library, where the next renewal can edit them:
ISSUE_DATE = "10/01/2026"
ROLL_PAIRS = {
("11/22/2025", "11/22/2026"): ("11/22/2026", "11/22/2027"),
}
UNCHANGED_PAIRS = {
("01/01/2026", "01/01/2027"), # not yet due, leave as is
}
Any policy row that fits neither pattern is left alone and listed as an outlier in the report rather than guessed at. Across the roughly 1,300 live certificates, every output passed the QA pass, and the files that needed a human decision were flagged in the report instead of being changed.
The QA pass writes a JSON and CSV report, and a self-test runs it against deliberately broken files to prove it catches problems:
python batch_roll_dates.py --dry-run # report only, writes nothing but the CSV
python batch_roll_dates.py --apply # write the mirror folder plus review crops
python qa_check.py # independent check of every pair
python tools/qa_selftest.py # prove qa_check.py fails when it should
What it means
- The time saved is the headline, but the durable win is that the process is repeatable and the rules are data, not code paths. Next renewal is an edit to three lines and a rerun.
- Editing a document in place is only as good as the proof that nothing else moved. Writing the checker separately, with its own reading of the rules, is what made the output something the agency could send without opening every file.
- Most of the difficulty was in the input, not the logic: two PDF generators, one file saved with the wrong extension, and dates that were drawings rather than text.
- The first tool shaped the second. Certificates the splitter wrote through PyPDF2 carry a subset CID font, which is why the rollover has to place replacement text as glyph ids at each original character’s position instead of just setting a string.