Skip to main content

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

TypeDescription
claim_paymentPayment to claimant or insured for a claim
commissionCommission payout to a producer
return_premiumPremium refund to insured (cancellation, endorsement)
vendorPayment to a third-party vendor (repair shop, adjuster)

Payment Methods

MethodDescription
checkPhysical check, printed via the platform
achAutomated Clearing House electronic transfer
wireWire 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

DecisionHTTP StatusResult
clear201Disbursement created normally
review201Disbursement created with pending_approval status; compliance event queued
block403Disbursement 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

StatusDescription
pendingCreated, awaiting approval
approvedApproved, ready for issuance
issuedACH batch transmitted or wire confirmed
voidedCancelled before settlement
failedACH 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:

  1. Validates that all payment IDs exist, are ACH method, and are in approved status.
  2. Fetches the payee bank account for each payment from payee_bank_accounts.
  3. Calls createACHBatch() from @openinsure/billing to generate the NACHA file content.
  4. Persists the batch and entry records to ach_batches and ach_entries.
  5. Updates all included payments to issued status.

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

OperationRequired Permission / Role
Create disbursementcreate_disbursement permission
Approve disbursementmanage_fiduciary permission
Print checkorg_admin or superadmin role
Generate positive paymanage_fiduciary permission
Create/approve paymentcreate_disbursement / manage_fiduciary
Manage ACH batchesmanage_fiduciary permission
Manage wire instructionsmanage_fiduciary permission
View paymentsorg_admin, superadmin, or adjuster role
  • Premium Billing -- Invoice generation and Stripe payment collection
  • Finance -- General ledger and TigerBeetle integration
  • Compliance -- Sanctions screening details
  • Claims -- Claim payment workflows