Premium Billing
The @openinsure/billing package handles the full billing lifecycle: installment schedule creation, invoice generation, Stripe payment collection, NSF handling, commission accounting, premium financing, and fiduciary trust account management.
Installment Plan Types
When a policy is bound, the billing module creates an installment schedule based on the selected plan:
| Plan | Code | Down Payment | Remaining Payments | Service Fee |
|---|---|---|---|---|
| Paid in Full | PIF | 100% | None | None |
| 2-Pay | TWO_PAY | 50% | 1 × 50% at 6 months | None |
| Quarterly | QUARTERLY | 25% | 3 × 25% quarterly | $5/installment |
| Monthly (10-pay) | MONTHLY_10 | 20% (2 months) | 8 × 10% monthly | $8/installment |
| Monthly (12-pay) | MONTHLY_12 | ~8.33% | 11 × 8.33% monthly | $8/installment |
| Premium Finance | PREMIUM_FINANCE | Down per PFA | Per PFA schedule | Per PFA rate |
The schedule is stored in the billing_schedules table. Each installment has a due_date, amount, status (pending, invoiced, paid, overdue, waived), and a stripe_payment_intent_id when collected.
Create Installment Schedule
The schedule is automatically created when POST /v1/submissions/:id/bind succeeds. You can also create or modify it:
POST /v1/policies/:id/billing-schedule
Authorization: Bearer <token>
Content-Type: application/json
{
"plan": "MONTHLY_10",
"downPaymentDate": "2025-06-01",
"paymentMethod": "pm_1Nk..." // Stripe PaymentMethod ID
}
Stripe Integration
OpenInsure uses Stripe PaymentIntents for all premium collection. The integration is in packages/billing/src/stripe/.
Payment Flow
1. Invoice becomes due
│
▼
2. POST /v1/invoices/:id/payment-intent
→ Creates Stripe PaymentIntent with amount, currency, metadata
→ Stores payment_intent_id on invoice
│
▼
3. Frontend confirms PaymentIntent
(card UI, ACH, or saved payment method)
│
▼
4. Stripe webhook: payment_intent.succeeded
→ Worker processes event
→ Invoice marked `paid`
→ Receipt generated
→ Commission calculated and posted to ledger
Creating a PaymentIntent
POST /v1/invoices/:id/payment-intent
Authorization: Bearer <token>
# Response:
{
"clientSecret": "pi_3Nk_secret_...",
"paymentIntentId": "pi_3NkX...",
"amount": 142500,
"currency": "usd"
}
Stripe Webhook Events Handled
| Event | Action |
|---|---|
payment_intent.succeeded | Mark invoice paid, post commission, send receipt |
payment_intent.payment_failed | Mark invoice failed, trigger dunning |
charge.dispute.created | Flag policy for review, notify UW |
charge.refunded | Post return premium to ledger |
setup_intent.succeeded | Store verified payment method for recurring billing |
Webhook Endpoint
Stripe webhooks are received at POST /v1/webhooks/stripe. The Worker validates the Stripe-Signature header before processing:
const event = stripe.webhooks.constructEvent(
rawBody,
request.headers.get('Stripe-Signature'),
env.STRIPE_WEBHOOK_SECRET
);
Premium Financing
For commercial accounts where the full annual premium is large, OpenInsure integrates with Premium Finance Agreements (PFAs). The insured borrows the net premium from a premium finance company (PFC), pays the PFC in installments, and the MGA receives the full premium upfront.
PFA Workflow
- Producer selects "Premium Finance" as the installment plan.
- The portal generates a PFA quote via the configured PFC integration (e.g., AFCO, First Insurance Funding).
- Insured signs the PFA electronically.
- PFC wires the net premium to the MGA trust account.
- Billing module records
PIFpayment against the policy invoice. - If the insured defaults, the PFC sends a
PREMIUM_FINANCE_CANCELLATIONnotice — the system triggers a 10-day notice of cancellation workflow.
Commission Accounting
Every policy transaction posts commission entries to the commission_ledger table.
Commission Types
| Type | Description | Timing |
|---|---|---|
PRODUCER_COMMISSION | Percentage of gross premium to the producer | Earned pro-rata with premium |
MGA_OVERRIDE | MGA's retained spread above producer commission | Earned pro-rata |
CARRIER_NET | Net premium remitted to carrier | Earned pro-rata |
CONTINGENT | Profit-sharing based on loss ratio | Calculated at year-end |
MGAFEE | Flat policy fee retained by MGA | 100% earned at inception |
Earned vs. Unearned Premium
Commission is earned on a pro-rata basis across the policy term. If a policy is cancelled:
- Earned commission = Commission × (days in force / total days)
- Unearned commission must be returned to the carrier
The billing module automatically calculates the return commission amount when a cancellation is processed and creates a corresponding debit entry in the commission ledger.
Commission Dashboard API
GET /v1/analytics/:orgId/commissions?period=2025-Q1
# Response:
{
"period": "2025-Q1",
"grossWrittenPremium": 1250000,
"producerCommissions": 125000, # 10%
"mgaOverride": 62500, # 5%
"carrierNet": 1062500, # 85%
"unearned": 410000, # estimated at period end
"earned": 840000
}
Fiduciary Trust Account Management
MGAs are fiduciaries for premium they collect on behalf of carriers. OpenInsure maintains a shadow trust ledger that reconciles against the actual bank account.
Trust Account Entries
Every premium receipt, commission deduction, carrier remittance, and refund is posted to the trust_ledger table:
CREATE TABLE trust_ledger (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
org_id uuid NOT NULL,
entry_type text NOT NULL, -- 'RECEIPT' | 'COMMISSION' | 'REMITTANCE' | 'REFUND' | 'ADJUSTMENT'
policy_id uuid,
invoice_id uuid,
amount numeric(15,2) NOT NULL, -- positive = inflow, negative = outflow
balance_after numeric(15,2) NOT NULL,
memo text,
created_at timestamptz NOT NULL DEFAULT now()
);
Carrier Remittance Reports
At the end of each remittance cycle (typically monthly), the billing module generates a carrier remittance report:
POST /v1/remittances/generate
Authorization: Bearer <admin_token>
Content-Type: application/json
{
"carrierId": "car_01J8...",
"periodStart": "2025-05-01",
"periodEnd": "2025-05-31"
}
The report lists every bound policy, endorsement premium, cancellation return, and commission deduction, with a final net amount to wire to the carrier.
Double-Entry Ledger (TigerBeetle)
The trust_ledger SQL table shown above is the PlanetScale-side reconciliation record. The authoritative double-entry ledger is TigerBeetle, a purpose-built financial database running on Fly.io. Every premium dollar is accounted for in both systems, but TigerBeetle is the source of truth for balances.
Fiduciary Split Flow
When a premium payment is received, calculateFiduciarySplit() breaks the gross amount into 5-6 buckets. The split is posted atomically to TigerBeetle as a set of transfers debiting MGA_FIDUCIARY and crediting the destination accounts:
Payment received ($10,000 gross)
-> MGA_FIDUCIARY (debit: $10,000)
-> CARRIER_PAYABLE (credit: $7,500) 85% net to carrier
-> PRODUCER_PAYABLE (credit: $1,000) 10% broker commission
-> MGA_REVENUE (credit: $500) 5% MGA commission
-> TAX_AUTHORITY_PAYABLE (credit: $500) 5% premium tax
-> (fees, stamping fees as applicable)
All amounts are in integer cents — no floating-point arithmetic touches financial data.
Account Types
Each organization is provisioned with 9 TigerBeetle accounts:
| Code | Account Type | Purpose |
|---|---|---|
| 1 | CARRIER_PAYABLE | Net premium owed to the carrier |
| 2 | MGA_FIDUCIARY | Hub account — all premium receipts land here first |
| 3 | MGA_REVENUE | MGA commission and fee income |
| 4 | PRODUCER_PAYABLE | Commissions owed to producers |
| 5 | TAX_AUTHORITY_PAYABLE | Premium taxes owed to state authorities |
| 6 | LOSS_FUND | Captive loss fund assets |
| 7 | CLAIMS_PAID | Cumulative claim disbursements |
| 8 | RESERVES | Outstanding loss reserves |
| 9 | REINSURER_PAYABLE | Reinsurance premiums owed to reinsurers |
Transfer Codes
| Code | Name | Description |
|---|---|---|
| 101 | PREMIUM_PAYMENT | Gross premium receipt into the fiduciary account |
| 102 | COMMISSION_SPLIT | Commission payout to producer or MGA |
| 103 | TAX_SPLIT | Premium tax allocation to tax authority |
| 104 | FEE_SPLIT | Fee allocation (stamping, policy, MGA) |
| 201 | SETTLEMENT | Claim settlement disbursement |
Claim Transactions
recordClaimTransaction supports four transaction types:
- payment — Debits
LOSS_FUNDand creditsCLAIMS_PAIDwhen a claim payment is issued. - reserve_adjustment — Moves funds between
RESERVESandLOSS_FUNDwhen case reserves change. - recovery — Credits
LOSS_FUNDwhen a subrogation or salvage recovery is received. - reinsurance_recovery — Debits
REINSURER_PAYABLEand creditsLOSS_FUNDwhen a stop-loss or treaty recovery is received.
NSF and Return Premium Handling
NSF (Non-Sufficient Funds)
When a Stripe payment fails:
- The invoice is marked
FAILED. - A dunning sequence begins: reminder at Day 1, warning at Day 5, final notice at Day 9.
- If the invoice remains unpaid at Day 10, a cancellation for non-payment is initiated (state notice requirements apply).
- An NSF fee (configurable, typically $25–$35) is added to the outstanding balance.
# Retry a failed invoice
POST /v1/invoices/:id/retry
Authorization: Bearer <token>
# Waive the NSF fee
POST /v1/invoices/:id/waive-fee
Authorization: Bearer <admin_token>
Content-Type: application/json
{ "reason": "First-time incident, customer has paid balance" }
Return Premium
Return premium arises from cancellations, endorsements that reduce coverage, and audit adjustments that result in lower payroll/revenue than estimated.
POST /v1/invoices/:id/refund
Authorization: Bearer <token>
Content-Type: application/json
{
"amount": 412.50,
"reason": "PRO_RATA_CANCELLATION",
"refundMethod": "ORIGINAL_PAYMENT" // or "CHECK" or "CREDIT"
}
Return premium in excess of 30 days after the original payment is typically issued by check per
state regulations. The refundMethod field controls this — the compliance engine will override
ORIGINAL_PAYMENT with CHECK if the original charge is too old to reverse.