Certificates of Insurance
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.
Certificate Format
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:
- Producer information block (name, address, phone, email)
- Insured information block
- Coverage table with carrier, policy number, effective/expiration dates, and limits
- Certificate holder block (named insured/mortgagee/loss payee)
- Additional insured endorsement checkbox
- Subrogation waiver checkbox
- Special provisions / description of operations
- ACORD disclaimer notice
API
buildCOIHTML(data: COIData): string
Generates 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): string
Creates a unique, sortable certificate number from the org's NAIC prefix:
Format: {ORG_PREFIX}-{TIMESTAMP_BASE36}-{RANDOM}
Example: MHCI-ZYXWVU-ABC1
The timestamp component (base-36 encoded) guarantees chronological sortability. The random suffix prevents collisions on high-volume issuance.
validateCOIRequest(input: unknown): COIRequest
Validates 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 input
batchIssueCOIs(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
);
Types
COIRequest
interface 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
}
COICoverage
interface 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;
}
COILimit
interface 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.
Security
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.
COI Requests in the API
Certificates are requested through the API and stored in the coi database table:
# Request a certificate
POST /v1/policies/:id/coi
Content-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.
Portal Integration
COIs can be requested from three surfaces:
- Policyholder Portal —
/certificatespage, self-service with standard holder template - Producer Portal —
/coipage, full holder customization and batch issuance - Underwriting Workbench — policy jacket → Coverages section → "Issue COI" button
COIs are evidence of insurance, not the policy itself. They do not expand, modify, or alter the coverage afforded by the referenced policies. Verify policy terms before issuing certificates with special provisions.
Documents
PDF rendering, e-signature, and R2 storage.
Policy Lifecycle
Policy issuance, endorsements, and cancellation.