Skip to content

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.

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

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

Section titled “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

Section titled “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[]>

Section titled “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
);
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
}
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;
}
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.

All user-supplied strings (insured name, special provisions, etc.) pass through escapeHtml() before insertion into the HTML template:

& → &amp; < → &lt; > → &gt; " → &quot; ' → &#x27;

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:

Terminal window
# 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.

COIs can be requested from three surfaces:

  1. Policyholder Portal/certificates page, self-service with standard holder template
  2. Producer Portal/coi page, full holder customization and batch issuance
  3. Underwriting Workbench — policy jacket → Coverages section → “Issue COI” button

Documents

PDF rendering, e-signature, and R2 storage.

Policy Lifecycle

Policy issuance, endorsements, and cancellation.