Disbursements & Payments
The disbursements and payments system handles all money-out operations: claim payments, commission payouts, return premiums, and vendor payments. It enforces OFAC sanctions screening before every disbursement, dual-control approval for high-value transactions, and supports three payment methods: check, ACH, and wire. The system is implemented across two route groups: apps/api/src/routes/disbursements.ts and apps/api/src/routes/payments.ts.
Disbursements
Disbursements represent approved outflows of money. Every disbursement goes through sanctions screening before creation and may require dual-control approval.
Creating a Disbursement
POST /v1/disbursements
Authorization: Bearer <token>
Content-Type: application/json
{
"type": "claim_payment",
"method": "check",
"payeeType": "claimant",
"payeeName": "John Smith",
"payeeAddress": "123 Main St, Tampa, FL 33602",
"amount": 15000,
"memo": "Claim #CLM-2026-001 settlement",
"referenceId": "claim-uuid",
"referenceType": "claim"
}
Disbursement Types
| Type | Description |
|---|---|
claim_payment | Payment to claimant or insured for a claim |
commission | Commission payout to a producer |
return_premium | Premium refund to insured (cancellation, endorsement) |
vendor | Payment to a third-party vendor (repair shop, adjuster) |
Payment Methods
| Method | Description |
|---|---|
check | Physical check, printed via the platform |
ach | Automated Clearing House electronic transfer |
wire | Wire transfer for large or time-sensitive payments |
Sanctions Screening
Every disbursement is screened against OFAC and international sanctions lists before the record is created. The screening uses checkSanctions() from @openinsure/compliance, which checks against the OpenSanctions API or a static fallback list.
Screening Flow
Create disbursement request
-> checkSanctions(payeeName, entityType)
-> resolveDecision(result)
-> Log to sanctions_audit_log (every check, for compliance evidence)
-> Decision: clear / review / block
Decision Outcomes
| Decision | HTTP Status | Result |
|---|---|---|
clear | 201 | Disbursement created normally |
review | 201 | Disbursement created with pending_approval status; compliance event queued |
block | 403 | Disbursement rejected; compliance.sanctions_hit event queued |
When a payee is blocked, the response includes match details:
{
"error": "Disbursement blocked: sanctions screening hit.",
"hits": [
{
"entryId": "ofac-12345",
"matchedOn": "name",
"matchType": "exact",
"score": 0.98,
"programs": ["SDN", "SDGT"]
}
],
"auditId": "uuid"
}
Sanctions Audit Log
Every sanctions screen -- clear, review, or block -- is logged to the sanctions_audit_log table with the full match details. This is a regulatory requirement: the audit log provides compliance evidence that every payee was screened before money moved.
The audit log entry is linked to the disbursement via referenceId after creation.
Dual-Control Approval
Disbursements require dual-control approval when either condition is met:
- Amount >= $10,000 (the
DUAL_APPROVAL_THRESHOLD) - Sanctions screening returned
review(probable match requiring human review)
Disbursements requiring approval are created with status pending_approval. The preparer's user ID is stored in preparedBy.
Approving a Disbursement
POST /v1/disbursements/:id/approve
Authorization: Bearer <token>
The approval endpoint enforces segregation of duties: the approver must be a different user than the preparer. If the same user attempts to approve their own disbursement, the request is rejected with 403:
{
"error": "Dual-control violation: Approver cannot be the same as preparer"
}
Status Flow
pending_approval -> approved -> printed (checks) / issued (ACH/wire)
Check Printing
Approved check disbursements can be printed as PDF documents.
GET /v1/disbursements/:id/print
The endpoint generates a check using buildCheckHTML() from @openinsure/documents:
- Payee name and address from the disbursement record.
- Amount in words via
amountToWords()from@openinsure/billing. - MICR line with check number, routing number, and account number.
- Drawer information -- the MGA trust account details.
If Cloudflare Browser Rendering is available (BROWSER binding), the HTML is rendered to PDF. Otherwise, the raw HTML is returned.
After printing, the disbursement status transitions from approved to printed and the check number is recorded.
Positive Pay File
For fraud prevention, the platform generates positive pay files that banks use to validate presented checks:
GET /v1/disbursements/positive-pay
This endpoint calls generatePositivePayFile() from @openinsure/billing with all checks printed today. The response is a downloadable text file in the bank's expected format.
Payments (Claims)
The payments system (/v1/billing/payments) provides a more granular payment management layer with ACH batching and wire transfer support.
Creating a Payment
POST /v1/billing/payments
{
"claimId": "uuid",
"payeeType": "claimant",
"payeeName": "Jane Doe",
"amount": 5000
}
If method is omitted, the system auto-selects based on selectPaymentMethod() from @openinsure/billing:
- If the payee has a bank account on file, ACH is preferred.
- Otherwise, check is used as the default.
Payment Lifecycle
| Status | Description |
|---|---|
pending | Created, awaiting approval |
approved | Approved, ready for issuance |
issued | ACH batch transmitted or wire confirmed |
voided | Cancelled before settlement |
failed | ACH returned or wire rejected |
Payments also enforce dual-control for amounts >= $10,000 -- the approver cannot be the same user as the creator.
Voiding a Payment
PATCH /v1/billing/payments/:id/void
{
"reason": "Duplicate payment identified"
}
Only payments in pending, approved, or issued status can be voided.
ACH Batches
Multiple approved ACH payments can be batched into a single NACHA file for bank submission.
Creating a Batch
POST /v1/billing/ach/batches
{
"paymentIds": ["uuid-1", "uuid-2", "uuid-3"],
"effectiveDate": "260324",
"entryClass": "CCD"
}
The endpoint:
- Validates that all payment IDs exist, are ACH method, and are in
approvedstatus. - Fetches the payee bank account for each payment from
payee_bank_accounts. - Calls
createACHBatch()from@openinsure/billingto generate the NACHA file content. - Persists the batch and entry records to
ach_batchesandach_entries. - Updates all included payments to
issuedstatus.
NACHA File Download
GET /v1/billing/ach/batches/:id/file
Returns the generated NACHA file as text/plain for upload to the bank portal.
ACH Returns
When a bank returns an ACH entry, the return is processed via:
POST /v1/billing/ach/returns
{
"entryId": "uuid",
"returnCode": "R01"
}
processACHReturn() from @openinsure/billing classifies the return code as hard (permanent failure like R01 Insufficient Funds) or soft (correctable like R02 Account Closed). The ACH entry is marked returned and the linked payment moves to failed.
Wire Transfers
For large or urgent payments, wire instructions can be created and confirmed.
Wire instructions are created via POST /v1/billing/wire/instructions with beneficiary name, bank, routing number, encrypted account number, optional SWIFT code, and a reference. After the wire is sent through the bank portal, POST /v1/billing/wire/instructions/:id/confirm records the confirmation timestamp and confirming user, and updates the linked payment to issued status.
Payee Bank Accounts
Bank accounts are stored encrypted in the payee_bank_accounts table. Only the last 4 digits of the account number are stored in plaintext for display purposes.
# List accounts for a payee
GET /v1/billing/payees/:id/accounts
# Add a new account
POST /v1/billing/payees/:id/accounts
{
"payeeType": "vendor",
"payeeName": "ABC Repair Shop",
"routingNumber": "021000089",
"accountNumberEncrypted": "encrypted-value",
"accountNumberLast4": "4567",
"accountType": "checking",
"isPreferred": true
}
Permissions
| Operation | Required Permission / Role |
|---|---|
| Create disbursement | create_disbursement permission |
| Approve disbursement | manage_fiduciary permission |
| Print check | org_admin or superadmin role |
| Generate positive pay | manage_fiduciary permission |
| Create/approve payment | create_disbursement / manage_fiduciary |
| Manage ACH batches | manage_fiduciary permission |
| Manage wire instructions | manage_fiduciary permission |
| View payments | org_admin, superadmin, or adjuster role |
Related
- Premium Billing -- Invoice generation and Stripe payment collection
- Finance -- General ledger and TigerBeetle integration
- Compliance -- Sanctions screening details
- Claims -- Claim payment workflows