Bank Accounts & Linking

Connect insured bank accounts for direct debits. The system uses a two-layer model:

  • Bank Accounts -- High-level account metadata (name, bank name, connection type, status). Used for display and association.
  • ACH Accounts -- Encrypted credential layer (routing number, account number, encrypted at rest with AES-256-GCM). Used for actual money movement.

Use the Plaid bank linking widget for instant, verified bank connections or add accounts manually.


Embed Plaid's bank linking widget in your UI. The insured selects their bank, authenticates, and you get verified account details -- no manual entry needed. Supports 10,000+ financial institutions.

POST /api/v1/plaid/link-token
Field Type Required Description
user_id string Yes Your internal user identifier
curl -X POST /api/v1/plaid/link-token \
  -H "X-Client-ID: $CLIENT_ID" \
  -H "X-Client-Secret: $CLIENT_SECRET" \
  -d '{ "user_id": "your_internal_user_id" }'
{
  "ok": true,
  "data": {
    "link_token": "link-sandbox-12345-abcde..."
  }
}

Step 2: Open the Bank Linking Widget in Your Frontend #

// Install: npm install react-plaid-link
import { usePlaidLink } from "react-plaid-link";

const { open } = usePlaidLink({
  token: linkToken,  // from Step 1
  onSuccess: (publicToken, metadata) => {
    // Send publicToken to your backend
    exchangeToken(publicToken, metadata);
  },
});

open();

See the Plaid Link documentation for full frontend integration details and supported frameworks.

Step 3: Exchange the Public Token #

POST /api/v1/plaid/exchange
Field Type Required Description
public_token string Yes Public token from Plaid Link onSuccess callback
user_id string Yes Your internal user identifier
employer_id string No Employer ObjectId to associate the connection with
carrier_id string No Carrier ObjectId (if linking for a carrier)
curl -X POST /api/v1/plaid/exchange \
  -H "X-Client-ID: $CLIENT_ID" \
  -H "X-Client-Secret: $CLIENT_SECRET" \
  -d '{
    "public_token": "public-sandbox-12345-abcde...",
    "user_id": "your_internal_user_id",
    "employer_id": "681xyz789abc123456789012"
  }'

This creates a permanent Plaid connection and automatically syncs the insured's bank accounts.

Step 4: Create an ACH Account from the Linked Bank #

POST /api/v1/bank-accounts/ach/from-plaid
Field Type Required Description
owner_type string Yes employer or carrier
owner_id string Yes Entity ObjectId
plaid_account_id string Yes Plaid account ID from the connection
plaid_item_id string Yes Plaid item ID from the connection
access_token string Yes Plaid access token (from exchange step)
nickname string No Friendly name for the account
is_primary boolean No Set as default account
curl -X POST /api/v1/bank-accounts/ach/from-plaid \
  -H "X-Client-ID: $CLIENT_ID" \
  -H "X-Client-Secret: $CLIENT_SECRET" \
  -d '{
    "owner_type": "employer",
    "owner_id": "681xyz789abc123456789012",
    "plaid_account_id": "acct_123...",
    "plaid_item_id": "item_456...",
    "access_token": "access-sandbox-abc123...",
    "nickname": "Operating Account",
    "is_primary": true
  }'

The ACH account is pre-verified (no micro-deposits needed) because credentials come directly from the linked bank via Plaid.


Manual Bank Account Entry #

For insureds who prefer not to use the bank linking widget.

Create a Bank Account (Metadata Layer) #

POST /api/v1/bank-accounts
Field Type Required Description
user_id string Yes User who owns or manages this account
name string Yes Account display name
bank_name string Yes Bank institution name
employer_id string No Employer ObjectId to associate with
carrier_id string No Carrier ObjectId to associate with
connection_type string No manual or plaid
primary boolean No Set as default account
curl -X POST /api/v1/bank-accounts \
  -H "X-Client-ID: $CLIENT_ID" \
  -H "X-Client-Secret: $CLIENT_SECRET" \
  -d '{
    "user_id": "user_abc123",
    "name": "Payroll Account",
    "bank_name": "Chase",
    "employer_id": "681xyz789abc123456789012"
  }'

Create an ACH Account (Credential Layer) #

POST /api/v1/bank-accounts/ach
Field Type Required Description
owner_type string Yes employer or carrier
owner_id string Yes Entity ObjectId
routing_number string Yes 9-digit routing number
account_number string Yes Bank account number (5-17 digits)
bank_name string Yes Bank institution name
account_type string Yes checking or savings
nickname string No Friendly name
is_primary boolean No Set as default account
curl -X POST /api/v1/bank-accounts/ach \
  -H "X-Client-ID: $CLIENT_ID" \
  -H "X-Client-Secret: $CLIENT_SECRET" \
  -d '{
    "owner_type": "employer",
    "owner_id": "681xyz789abc123456789012",
    "routing_number": "021000021",
    "account_number": "1234567890",
    "bank_name": "Chase",
    "account_type": "checking",
    "nickname": "Payroll Account"
  }'
{
  "ok": true,
  "data": {
    "_id": "684abc123def456789012345",
    "account_last4": "7890",
    "routing_last4": "0021",
    "status": "unverified",
    "bank_name": "Chase",
    "account_type": "checking"
  }
}

Note: Manually-entered ACH accounts start as unverified. Full routing and account numbers are never returned in API responses -- only the last 4 digits are shown.


Bank Account Management (Metadata Layer) #

List Bank Accounts #

GET /api/v1/bank-accounts?employer_id=...&carrier_id=...&connection_type=plaid&status=active&primary=true&page=1&limit=50

Get a Bank Account #

GET /api/v1/bank-accounts/{id}

Update a Bank Account #

PATCH /api/v1/bank-accounts/{id}

Set as Primary Account #

PATCH /api/v1/bank-accounts/{id}/default
{ "primary": true }

Delete a Bank Account #

DELETE /api/v1/bank-accounts/{id}

ACH Account Management (Credential Layer) #

List ACH Accounts #

GET /api/v1/bank-accounts/ach?owner_id=...&owner_type=employer&status=active&connection_type=plaid

Get an ACH Account #

GET /api/v1/bank-accounts/ach/{id}

Update an ACH Account #

PATCH /api/v1/bank-accounts/ach/{id}

Delete an ACH Account #

DELETE /api/v1/bank-accounts/ach/{id}

Plaid Connections & Transactions #

List Connections #

GET /api/v1/plaid/connections?employer_id=...&carrier_id=...&status=active&page=1&limit=50

Get a Connection #

GET /api/v1/plaid/connections/{id}

Update a Connection (Add Products) #

POST /api/v1/plaid/link-token/update
Field Type Required Description
user_id string Yes Your internal user identifier
item_id string Yes Plaid item ID to update

Sync Accounts from Connection #

POST /api/v1/plaid/sync-accounts
Field Type Required Description
item_id string Yes Plaid item ID to sync

Get Live Balances #

POST /api/v1/plaid/balance
Field Type Required Description
plaid_item_id string Yes Plaid item ID

View Bank Transactions #

GET /api/v1/plaid/transactions?employer_id=...&plaid_item_id=...&plaid_account_id=...&date_from=2026-01-01&date_to=2026-03-31&pending=false&page=1&limit=50

Get a Transaction #

GET /api/v1/plaid/transactions/{id}

Transaction Summary #

GET /api/v1/plaid/transactions/summary?employer_id=...&plaid_item_id=...&date_from=2026-01-01&date_to=2026-03-31
{
  "ok": true,
  "data": {
    "total_transactions": 847,
    "total_debits_cents": 12500000,
    "total_credits_cents": 15200000,
    "net_cents": 2700000
  }
}

Security #

  • Routing and account numbers are encrypted at rest using envelope encryption (AES-256-GCM)
  • Credentials are never returned in API responses -- only the last 4 digits are shown
  • All bank operations are logged without sensitive data
  • Plaid connection tokens are encrypted before storage
  • All API requests require tenant-scoped authentication -- you can only access accounts belonging to employers/carriers within your scope