Stop-Loss Insurance
Stop-loss (excess of loss) insurance protects self-funded plans and captives from catastrophic claims. The @openinsure/stop-loss package provides actuarial calculation functions for specific stop-loss, aggregate stop-loss, loss run reporting, and loss ratio analysis.
All financial amounts use the Money type (integer cents) from @openinsure/rating, eliminating floating-point arithmetic errors in financial calculations. Identifiers use branded types (StopLossPolicyId, ClaimantId, StopLossClaimId) for compile-time safety. Aggregate factors are expressed in basis points (e.g. 12500 bps = 125.00%) rather than float percentages.
Overview
Specific SL
Per-individual protection — the reinsurer pays losses above the specific attachment point per claimant. Supports lasered (individual-specific) attachment overrides.
Aggregate SL
Total loss protection — the reinsurer pays when cumulative losses exceed the aggregate attachment (typically 120–125% of expected losses, expressed in basis points).
Core Types
The package uses branded types to prevent accidental misuse of plain strings and numbers:
import {
type StopLossPolicyId,
type ClaimantId,
type AttachmentPoint,
stopLossPolicyId,
claimantId,
attachmentPoint,
} from '@openinsure/stop-loss';
import { fromCents } from '@openinsure/rating';
const policyId = stopLossPolicyId('pol_abc123');
const claimant = claimantId('clm_jane_doe');
const attachment = attachmentPoint(fromCents(25_000_00, 'USD')); // $250,000
| Type | Description |
|---|---|
Money | Integer cents with currency code — all financial amounts use this type from @openinsure/rating |
StopLossPolicyId | Branded string identifying a stop-loss policy |
ClaimantId | Branded string identifying a covered individual |
AttachmentPoint | Branded Money value representing a threshold |
StopLossClaimId | Branded string identifying a claim submission |
Specific Stop-Loss
Specific stop-loss protects against a single large claimant (health: single individual; casualty: single occurrence). Claims must strictly exceed the attachment point for reimbursement to apply.
Single Claim Reimbursement
import {
calculateSpecificReimbursement,
type SpecificStopLossPolicy,
type StopLossClaimSubmission,
stopLossPolicyId,
claimantId,
stopLossClaimId,
attachmentPoint,
} from '@openinsure/stop-loss';
import { fromCents } from '@openinsure/rating';
const policy: SpecificStopLossPolicy = {
id: stopLossPolicyId('pol_2026_001'),
attachmentPoint: attachmentPoint(fromCents(25_000_00, 'USD')), // $250,000
maximumBenefit: fromCents(200_000_00, 'USD'), // $2,000,000
policyPeriod: {
effectiveDate: new Date('2026-01-01'),
expirationDate: new Date('2027-01-01'),
},
coveredLives: 450,
laseredAttachments: [],
};
const claim: StopLossClaimSubmission = {
id: stopLossClaimId('sc_001'),
policyId: stopLossPolicyId('pol_2026_001'),
claimantId: claimantId('emp_4821'),
incurredAmount: fromCents(82_500_00, 'USD'), // $825,000
incurredDate: new Date('2026-06-15'),
submittedDate: new Date('2026-07-01'),
status: 'open',
description: 'Cardiac surgery and rehabilitation',
};
const result = calculateSpecificReimbursement(claim, policy);
// result.reimbursed → $575,000 (825K - 250K attachment)
// result.retained → $250,000 (attachment point)
// result.excess → $0 (within $2M max benefit)
Lasered Attachments
High-risk individuals can have a higher attachment point (lasering):
import { type LaseredAttachment } from '@openinsure/stop-loss';
const policy: SpecificStopLossPolicy = {
// ...same as above
laseredAttachments: [
{
claimantId: claimantId('emp_9102'),
attachmentPoint: attachmentPoint(fromCents(50_000_00, 'USD')), // $500,000 laser
},
],
};
Claims for emp_9102 are evaluated against the $500,000 lasered attachment instead of the standard $250,000 policy attachment.
Aggregating Costs by Claimant
Specific stop-loss attachment is evaluated on the per-individual total across all claims, not per-claim:
import { aggregateClaimantCosts } from '@openinsure/stop-loss';
const claimantTotals: Map<ClaimantId, Money> = aggregateClaimantCosts(allClaims, policy);
// Map { 'emp_4821' => $825,000, 'emp_1204' => $142,000, ... }
Key concepts:
| Term | Definition |
|---|---|
| Attachment point | The threshold — the captive/plan retains 100% up to this amount per individual |
| Maximum benefit | Maximum reinsurer payout per individual per policy period |
| Lasered attachment | Individual-specific attachment override for high-risk claimants |
| Retained | Amount below the attachment (plan's responsibility) |
| Excess | Amount above the maximum benefit (also plan's responsibility) |
Aggregate Stop-Loss
Aggregate stop-loss protects against adverse total loss experience for the year. The attachment factor is expressed in basis points to avoid floating-point arithmetic.
Calculating the Aggregate Attachment Point
import { calculateAggregateAttachmentPoint } from '@openinsure/stop-loss';
import { fromCents } from '@openinsure/rating';
const expectedClaims = fromCents(380_000_00, 'USD'); // $3,800,000
const factorBps = 12500; // 125.00%
const aggAttachment = calculateAggregateAttachmentPoint(expectedClaims, factorBps);
// aggAttachment → $4,750,000 (3,800,000 * 125%)
Aggregate Reimbursement
import {
calculateAggregateReimbursement,
calculateAggregateAttachmentPoint,
type AggregateStopLossPolicy,
} from '@openinsure/stop-loss';
const totalClaims = fromCents(520_000_00, 'USD'); // $5,200,000
const attachment = calculateAggregateAttachmentPoint(
fromCents(380_000_00, 'USD'), // expected claims
12500 // 125% factor
);
const corridorBps = 0; // no corridor
const maxBenefit = fromCents(700_000_00, 'USD'); // $7,000,000
const result = calculateAggregateReimbursement(totalClaims, attachment, corridorBps, maxBenefit);
// result.reimbursed → $450,000 (5.2M - 4.75M attachment)
// result.retained → $4,750,000
// result.excess → $0
Aggregate attachment is typically set at 120–125% of expected losses (12000–12500 bps), negotiated with the reinsurer based on historical loss ratio volatility.
Corridor
The corridor is a band above the attachment where no recovery occurs, also expressed in basis points:
// 500 bps corridor = 5% of the aggregate attachment
const corridorBps = 500;
// If attachment = $4,750,000, corridor = $237,500
// Effective threshold = $4,987,500
Monthly Accumulator
Track aggregate stop-loss on a month-by-month basis. The accumulator detects when cumulative claims breach the attachment and calculates per-month reimbursements:
import {
runAggregateAccumulator,
type AggregateStopLossPolicy,
stopLossPolicyId,
} from '@openinsure/stop-loss';
import { fromCents } from '@openinsure/rating';
const policy: AggregateStopLossPolicy = {
id: stopLossPolicyId('agg_2026_001'),
attachmentFactorBps: 12500, // 125%
expectedClaims: fromCents(380_000_00, 'USD'), // $3,800,000
corridorBps: 0,
minimumAggregate: fromCents(100_000_00, 'USD'),
maximumAggregate: fromCents(700_000_00, 'USD'),
policyPeriod: {
effectiveDate: new Date('2026-01-01'),
expirationDate: new Date('2027-01-01'),
},
policyMonths: 12,
};
const monthlyClaims = [
fromCents(30_000_00, 'USD'), // Jan: $300K
fromCents(35_000_00, 'USD'), // Feb: $350K
fromCents(42_000_00, 'USD'), // Mar: $420K
// ...remaining months
];
const entries = runAggregateAccumulator(monthlyClaims, policy);
for (const entry of entries) {
console.log(
`Month ${entry.month}: cumulative=${entry.cumulativeClaims.cents / 100}`,
`breached=${entry.attachmentBreached}`,
`reimbursement=${entry.reimbursementThisMonth.cents / 100}`
);
}
Each AccumulatorEntry includes:
| Field | Description |
|---|---|
month | Month number (1–12) |
claimsThisMonth | Claims incurred this month |
cumulativeClaims | Running total of all claims |
attachmentBreached | Whether cumulative claims exceed the aggregate attachment |
reimbursementThisMonth | Stop-loss recovery for this month (capped by maximum aggregate) |
cumulativeReimbursement | Running total of all reimbursements |
Loss Run Reporting
Generate a comprehensive loss run summary for a specific stop-loss policy:
import { generateLossRunSummary } from '@openinsure/stop-loss';
const summary = generateLossRunSummary(policy, allClaims);
// summary.totalEligibleClaims → 142
// summary.totalIncurred → $4,320,000
// summary.totalAboveAttachment → $1,175,000
// summary.totalBelowAttachment → $3,145,000
// summary.claimantCount → 89
// summary.claimantsExceedingAttachment → 4
// summary.statusBreakdown → [{ status: 'closed', count: 98, ... }, ...]
The LossRunSummary includes a status breakdown across all claims (including denied), while financial totals only count eligible claims within the policy period.
Loss Ratio Analysis
Calculate the loss ratio to evaluate stop-loss carrier performance:
import { calculateLossRatio } from '@openinsure/stop-loss';
import { fromCents } from '@openinsure/rating';
const result = calculateLossRatio(
fromCents(50_000_00, 'USD'), // $500,000 premium paid
fromCents(32_500_00, 'USD') // $325,000 reimbursements received
);
// result.lossRatioBps → 6500 (65.00%)
// result.lossRatioDecimal → 0.65
// result.premiumPaid → $500,000
// result.reimbursementsReceived → $325,000
A loss ratio above 10000 bps (100%) means the carrier paid out more than it collected in premium.
Loss ratios are a key metric for renewal negotiations. A consistently low loss ratio (<60%) may justify lower premiums at renewal, while a high loss ratio (>100%) may trigger attachment point increases or premium surcharges.
Integration with Captive Admin
Stop-loss calculations feed the Captive Admin module's Loss Fund dashboard:
- Monthly fund ratio tracking
- Specific SL recovery tracking per claimant
- Aggregate corridor visualization
- Actuarial projection (run-out of current year)
See Loss Fund Tracking for the dashboard reference.
Reinsurance Coordination
Stop-loss programs are often structured as part of the broader reinsurance program. The cession to the stop-loss reinsurer appears in the quarterly cession statement alongside quota share and XOL treaties.
See Reinsurance Management for how cession statements are compiled.