Documents
PDF rendering, e-signature, and R2 storage.
The @openinsure/coi package generates ACORD-compliant Certificates of Insurance (COIs). It produces print-ready HTML that is converted to PDF by @openinsure/documents, with built-in XSS sanitization and unique certificate numbering.
OpenInsure generates ACORD 25 (2016/03) format certificates — the industry-standard evidence of property and casualty insurance. The generated HTML matches the ACORD 25 layout exactly:
buildCOIHTML(data: COIData): stringGenerates the complete ACORD 25 HTML from structured coverage data.
import { buildCOIHTML } from '@openinsure/coi';import { money } from '@openinsure/rating';
const html = buildCOIHTML({ certificateNumber: 'MHCI-ZYX123-ABC', issueDate: '2026-03-08', producer: { name: 'MHC Insurance Group', address: '123 Main St', city: 'Columbia', state: 'SC', zip: '29201', phone: '(803) 555-0100', email: 'certs@mhcmga.com', }, insured: { name: 'Acme Hardware LLC', address: '456 Commerce Blvd', city: 'Charlotte', state: 'NC', zip: '28201', }, coverages: [ { type: 'Commercial General Liability', carrier: 'Travelers Indemnity Co', policyNumber: 'GL-2026-00451', effectiveDate: '2026-01-01', expirationDate: '2027-01-01', limits: [ { label: 'Each Occurrence', amount: money(1_000_000) }, { label: 'General Aggregate', amount: money(2_000_000) }, { label: 'Products-Comp/Op Agg', amount: money(2_000_000) }, ], additionalInsuredEndorsement: true, waiverOfSubrogation: false, }, ], certificateHolder: { name: 'City of Charlotte', address: '600 E 4th St', city: 'Charlotte', state: 'NC', zip: '28202', }, additionalInsureds: [{ name: 'City of Charlotte', relationship: 'Owner' }], specialProvisions: 'Certificate holder is named as additional insured per CG 20 26.', cancellationNoticeDays: 30,});The returned HTML is self-contained (inline CSS) and ready for PDF conversion:
import { renderHTMLToPDF } from '../lib/pdf-renderer';
const pdfBuffer = await renderHTMLToPDF(html);generateCertificateNumber(orgId: string): stringCreates a unique, sortable certificate number from the org’s NAIC prefix:
Format: {ORG_PREFIX}-{TIMESTAMP_BASE36}-{RANDOM}Example: MHCI-ZYXWVU-ABC1The timestamp component (base-36 encoded) guarantees chronological sortability. The random suffix prevents collisions on high-volume issuance.
validateCOIRequest(input: unknown): COIRequestValidates a raw COI request payload with Zod, throwing a structured error if required fields are missing or malformed.
const request = validateCOIRequest(rawBody);// Throws ZodError with field-level messages on invalid inputbatchIssueCOIs(requests: COIRequest[], policy: Policy): Promise<COIResult[]>Issues multiple certificates concurrently — useful for projects requiring COIs for multiple holders (e.g., a general contractor needing certs for each subcontractor).
const results = await batchIssueCOIs( holders.map((h) => ({ policyId: policy.id, certificateHolder: h, additionalInsured: true, })), policy);COIRequestinterface COIRequest { policyId: string; certificateHolder: { name: string; address: string; city: string; state: string; // 2-char state code zip: string; email?: string; }; additionalInsureds?: { name: string; relationship: 'Owner' | 'Lessor' | 'General Contractor' | 'Other'; }[]; specialProvisions?: string; // max 500 chars requestedBy: string; // user ID expiresAt?: string; // ISO date — defaults to policy expiration}COICoverageinterface COICoverage { type: string; // e.g., 'Commercial General Liability' carrier: string; policyNumber: string; effectiveDate: string; // YYYY-MM-DD expirationDate: string; limits: COILimit[]; additionalInsuredEndorsement: boolean; waiverOfSubrogation: boolean;}COILimitinterface COILimit { label: string; amount: Money; // from @openinsure/rating — integer cents}Limit amounts are Money objects ({ cents, currency }). The buildCOIHTML() function uses toDollars() internally to format amounts as currency strings in the rendered certificate.
All user-supplied strings (insured name, special provisions, etc.) pass through escapeHtml() before insertion into the HTML template:
& → & < → < > → > " → " ' → 'This prevents XSS attacks if a malicious string is entered in any COI field and then rendered in a browser before PDF conversion.
Certificates are requested through the API and stored in the coi database table:
# Request a certificatePOST /v1/policies/:id/coiContent-Type: application/json
{ "certificateHolder": { "name": "City of Charlotte", "address": "600 E 4th St", "city": "Charlotte", "state": "NC", "zip": "28202" }, "additionalInsured": true, "specialProvisions": "Named additional insured per CG 20 26."}
# Response: { "id": "...", "certificateNumber": "MHCI-...", "downloadUrl": "..." }The generated PDF is stored in Cloudflare R2 (BUCKET binding on oi-sys-api) and returned as a signed download URL valid for 1 hour.
COIs can be requested from three surfaces:
/certificates page, self-service with standard holder template/coi page, full holder customization and batch issuanceDocuments
PDF rendering, e-signature, and R2 storage.
Policy Lifecycle
Policy issuance, endorsements, and cancellation.