Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.encoreos.io/llms.txt

Use this file to discover all available pages before exploring further.

Version: 1.0.0
Created: 2026-01-28
Updated: 2026-01-29
Status: πŸ“‹ Planned
Owner: FA (Finance & Accounting)

Overview

This document defines the integration contract between Encore Health OS’s FA module and Plaid for automated bank feed integration. Plaid provides broad institution coverage (11,000+ globally), OAuth-based authentication, and a developer-friendly API.

Integration Pattern

Type: External API Integration via Edge Functions
Direction: Outbound (Encore Health OS β†’ Plaid) + Inbound (Webhooks)
Authentication: API Keys (client_id + secret)

Authentication Requirements

Environment Matrix

EnvironmentURLNotes
Sandboxsandbox.plaid.comTest data, no real banks
Developmentdevelopment.plaid.comReal banks, limited access
Productionproduction.plaid.comFull access, requires approval

Credentials

CredentialPurposeStorage
PLAID_CLIENT_IDApplication identificationSupabase Secret
PLAID_SECRETAPI authenticationSupabase Secret
PLAID_ENVIRONMENTEnvironment selectionSupabase Secret
PLAID_WEBHOOK_SECRETWebhook signature verificationSupabase Secret

API Endpoints

Plaid API (Outbound)

EndpointMethodPurpose
/link/token/createPOSTCreate Link token for frontend
/item/public_token/exchangePOSTExchange public token for access token
/transactions/syncPOSTSync transactions (incremental)
/accounts/getPOSTGet account details
/accounts/balance/getPOSTGet account balances
/item/removePOSTDisconnect account
/transfer/authorization/createPOSTAuthorize a transfer (required before create)
/transfer/createPOSTCreate transfer using approved authorization_id
/transfer/intent/createPOSTCreate transfer intent for Transfer UI
/transfer/intent/getPOSTGet transfer intent status and transfer_id

Edge Functions (Internal)

FunctionMethodPurpose
plaid-create-link-tokenPOSTCreate Plaid Link token
plaid-exchange-tokenPOSTExchange public_token for access_token
plaid-syncPOSTSync transactions for account
plaid-disconnectPOSTDisconnect Plaid account
plaid-webhookPOSTHandle Plaid webhook events
hr-plaid-transfer-createPOSTCreate ACH credits for payroll (uses authorize then create)
plaid-transfer-intent-createPOSTCreate transfer intent for Transfer UI (one-time payment/disbursement)
plaid-transfer-intent-getPOSTGet transfer intent status and transfer_id after Transfer UI
plaid-create-link-tokenPOSTSupports transfer_intent_id + link_customization_name for Transfer UI

Transfer UI (One-Time Payment / Disbursement)

Transfer UI is Plaid’s hosted flow for one-time debit or credit. It is not used for batch payroll (see Programmatic Plaid Transfer below). Flow:
  1. Create intent: Frontend calls plaid-transfer-intent-create with organization_id, mode (PAYMENT | DISBURSEMENT), amount, description, ach_class, user (e.g. legal_name). Backend calls Plaid POST /transfer/intent/create and returns transfer_intent_id.
  2. Link token: Frontend calls plaid-create-link-token with organization_id, transfer_intent_id, and optional link_customization_name. Backend returns a link token with products: ["transfer"] and transfer.intent_id.
  3. Open Link: Frontend opens Plaid Link with that token; user authorizes the transfer in Plaid’s UI.
  4. Post-success: On Link onSuccess, metadata.transfer_status is available; frontend may call plaid-transfer-intent-get with transfer_intent_id to get final status, transfer_id, and failure_reason.
Configuration:
  • Origination account: Required for transfer intent. Resolved in order: (1) fa_module_settings.plaid_origination_account_id for the organization, (2) env PLAID_ORIGINATION_ACCOUNT_ID. At least one must be set.
  • Link customization: In Plaid Dashboard, create a Link customization with Account Select: Enabled for one account. Optionally set fa_module_settings.plaid_link_customization_name per org, or pass link_customization_name when requesting the link token / from frontend (useTransferUI / TransferUIButton).
Frontend: Platform banking exposes useTransferUI and TransferUIButton from @/platform/banking; they orchestrate intent create β†’ link token β†’ Link open β†’ optional intent/get.

Programmatic Plaid Transfer (HR Payroll)

Payroll direct deposit uses Plaid Transfer in a two-step flow. Per Plaid: Creating transfers:
  1. Authorize: Call POST /transfer/authorization/create with access_token, account_id, type, network, amount, ach_class, user, and an idempotency_key. Plaid returns authorization_id and decision (approved/declined).
  2. Create: If approved, call POST /transfer/create with authorization_id, access_token, account_id, and description only.
The hr-plaid-transfer-create edge function uses the shared plaid-client.ts (createTransferAuthorization, createTransfer) and passes an idempotency key per payment item (e.g. hr-pay-{batchId}-{itemId}) to avoid duplicate transfers on retries. ACH description is limited to 10 characters (e.g. PAY + batch number suffix).

Data Flow

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                 β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚   Encore Health OS    β”‚                 β”‚   Plaid API     β”‚
β”‚   Frontend      β”‚                 β”‚                 β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜                 β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜
         β”‚                                    β”‚
         β”‚ 1. Request Link Token              β”‚
         β”‚    (to Edge Function)              β”‚
         β–Ό                                    β”‚
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                           β”‚
β”‚ plaid-create-   β”‚ 2. POST /link/token/createβ”‚
β”‚ link-token      β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Ίβ”‚
β”‚                 β”‚                           β”‚
β”‚                 β”‚ 3. link_token             β”‚
β”‚                 │◄───────────────────────────
β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜                           β”‚
         β”‚                                    β”‚
         β”‚ 4. link_token to frontend          β”‚
         β–Ό                                    β”‚
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                           β”‚
β”‚   Plaid Link    β”‚ 5. User authenticates     β”‚
β”‚   (Frontend)    β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Ίβ”‚
β”‚                 β”‚                           β”‚
β”‚                 β”‚ 6. public_token           β”‚
β”‚                 │◄───────────────────────────
β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜                           β”‚
         β”‚                                    β”‚
         β”‚ 7. POST /plaid-exchange-token      β”‚
         β–Ό                                    β”‚
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” 8. POST /item/public_     β”‚
β”‚ plaid-exchange- β”‚    token/exchange         β”‚
β”‚ token           β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Ίβ”‚
β”‚                 β”‚                           β”‚
β”‚                 β”‚ 9. access_token + accountsβ”‚
β”‚                 │◄───────────────────────────
β”‚                 β”‚                           β”‚
β”‚                 β”‚ 10. Store in fa_bank_accounts
β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜                           β”‚
         β”‚                                    β”‚
         β”‚ 11. Webhook: INITIAL_UPDATE        β”‚
         │◄────────────────────────────────────
         β”‚                                    β”‚
         β–Ό                                    β”‚
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” 12. POST /transactions/sync
β”‚   plaid-sync    β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Ίβ”‚
β”‚                 β”‚                           β”‚
β”‚                 β”‚ 13. transactions + cursor β”‚
β”‚                 │◄───────────────────────────
β”‚                 β”‚                           β”‚
β”‚                 β”‚ 14. Store in fa_bank_statement_lines
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Webhook Events

Supported Events

EventAction
INITIAL_UPDATETrigger initial transaction sync
HISTORICAL_UPDATETrigger historical transaction sync
DEFAULT_UPDATETrigger incremental transaction sync
TRANSACTIONS_REMOVEDMark removed transactions
ITEM_LOGIN_REQUIREDSet status to reauth_required
ERRORLog error, potentially set status to error

Signature Verification

Header: Plaid-Verification The webhook body is signed using JWT. Verification requires:
  1. Fetch Plaid’s verification key
  2. Verify JWT signature
  3. Validate claims (iat, request_body_sha256)
// Webhook verification (simplified)
const verifyPlaidWebhook = async (body: string, verification: string) => {
  // 1. Decode JWT header to get key_id
  // 2. Fetch verification key from Plaid
  // 3. Verify JWT signature
  // 4. Verify request_body_sha256 matches
  return isValid;
};

Database Schema

fa_bank_accounts (Additional Columns)

ColumnTypePurpose
plaid_account_idTEXTPlaid account ID
plaid_item_idTEXTPlaid Item ID (groups accounts)
plaid_access_tokenTEXTAPI access token (encrypted)
plaid_institution_idTEXTInstitution identifier
plaid_sync_enabledBOOLEANAuto-sync enabled flag
plaid_last_synced_atTIMESTAMPTZLast sync timestamp
plaid_cursorTEXTSync API pagination cursor
plaid_connection_statusTEXTconnected/disconnected/error/reauth_required
bank_providerTEXTmanual/teller/plaid

fa_bank_statement_lines (Additional Column)

ColumnTypePurpose
plaid_transaction_idTEXTPlaid transaction ID for deduplication

fa_module_settings (Additional Columns)

ColumnTypePurpose
plaid_integration_enabledBOOLEANFeature flag
plaid_origination_account_idTEXTTransfer UI: Plaid origination (funding) account ID; overrides env when set
plaid_link_customization_nameTEXTTransfer UI: Link customization name (Account Select: one account) from Plaid Dashboard

Security Considerations

Token Storage

  • Access tokens stored in plaid_access_token column (encrypted at rest by Supabase)
  • Tokens never exposed to frontend
  • Edge functions access tokens via service role

Multi-Tenancy

  • All Edge Function queries include organization_id filter
  • RLS policies prevent cross-tenant access
  • Audit logging for all Plaid operations

Webhook Security

  • JWT signature verification
  • Request body hash validation
  • Rate limiting via Supabase

Institution-specific behavior (Duplicate Items)

For Chase, PNC, Navy Federal Credit Union, and Charles Schwab, linking the same credentials again (creating a second Plaid Item for the same institution) can invalidate the existing Item at the time the user completes the institution’s OAuth flow. When this occurs:
  • Option 1: Use Plaid’s update mode to repair the existing Item (re-launch Link with the existing access_token). That will invalidate the new Item; then remove the new Item via /item/remove if it was already created.
  • Option 2: Remove the old Item using Plaid’s /item/remove endpoint and use the new Item.
Encore Health OS prevents duplicate Items per organization by checking plaid_institution_id before exchanging the public token (see duplicate prevention in implementation). This runbook is for awareness when handling support cases or when adding Item-level health handling (e.g. webhooks) that may replace an invalidated Item.

Error Handling

Plaid API Errors

Error CodeMeaningAction
ITEM_LOGIN_REQUIREDUser needs to re-authenticateSet status to reauth_required
INVALID_ACCESS_TOKENToken invalid or revokedSet status to disconnected
RATE_LIMIT_EXCEEDEDToo many requestsExponential backoff, retry
INTERNAL_SERVER_ERRORPlaid errorLog, retry with backoff

Retry Strategy

// Exponential backoff for rate limiting
const delays = [1000, 2000, 4000, 8000, 16000]; // ms
for (const delay of delays) {
  const result = await callPlaidApi();
  if (result.error_code !== 'RATE_LIMIT_EXCEEDED') break;
  await sleep(delay);
}

Logging

Allowlisted Fields (Safe to Log)

  • bank_account_id
  • plaid_item_id (not access token)
  • plaid_account_id
  • transactions_synced
  • sync_duration_ms

Prohibited (Never Log)

  • plaid_access_token
  • plaid_secret
  • Full account numbers

Testing Strategy

Sandbox Testing

  • Use Plaid sandbox environment
  • Test institutions: ins_109508 (Chase), ins_109511 (Bank of America)
  • Test credentials: user_good / any password

Integration Tests

  • Link token creation
  • Token exchange
  • Transaction sync with deduplication
  • Webhook signature verification

E2E Tests

  • Full connect β†’ sync β†’ disconnect flow

Rollback Strategy

All Plaid columns are nullable. Rollback procedure:
  1. Disable feature flag: plaid_integration_enabled = false
  2. UI hides all Plaid components
  3. Manual CSV/OFX import continues to work
  4. No data migration required

Account selection and GL mapping

After Plaid Link succeeds, Encore Health OS uses a deferred exchange flow when onPlaidLinkSuccess is set:
  1. Link returns public_token and metadata.accounts (all accounts the user linked at the institution).
  2. The app shows PlaidAccountMappingDialog: user selects which accounts to connect and assigns a GL account to each.
  3. Only selected accounts are sent to plaid-exchange-token with gl_account_id per account.
  4. Backend creates fa_bank_accounts rows and triggers initial sync for each.
Backend contract: plaid-exchange-token accepts accounts[] with optional gl_account_id. If provided, that GL is used (and must be an asset, posting-allowed account in the same organization); otherwise the server falls back to default cash/asset lookup. Plaid docs: metadata.accounts in the Link onSuccess callback contains id, name, mask, type, subtype. Account selection in Link is controlled by the user inside Plaid’s UI; we do not filter which accounts appear in Link for initial link (optional: use account_filters when creating the link token to limit types/subtypes).

Recommendations (from Plaid docs and product)

RecommendationPurpose
Account/GL mapping dialogLet users choose which linked accounts to import and which GL to use (implemented).
account_filters (optional)When creating the link token, pass account_filters (e.g. depository.account_subtypes: ['checking','savings'], credit.account_subtypes: ['credit card']) so Link only shows relevant account types.
transactions.days_requestedRequest up to 730 days of history in link token if product supports it; improves initial sync.
Update modeFor existing Items, use update.account_selection_enabled: true and access_token when creating the link token so users can add/remove accounts without reconnecting.
Link customizationUse link_customization_name for a consistent/branded Link experience if configured in Plaid Dashboard.
Error surfacingWhen exchange returns accounts: [], surface errors[] from the response (e.g. β€œNo GL account found…”) so users can fix setup (implemented).

Product initialization

We request Transactions and Balance when creating Link tokens. The Balance product is required for /accounts/balance/get and for populating fa_bank_accounts.balance; without it, Bank Summary and Cash Allocation widgets will show zero or omit the account. Existing Items created with only transactions do not have Balanceβ€”re-linking (or Plaid update mode with balance added) is needed to get balance. When we add Auth or Identity, consider optional_products (Auth) or required_if_supported_products (Identity) per Plaid’s Choosing when to initialize products.

Balance and cash position

When Plaid updates fa_bank_accounts.balance (in plaid-sync after a manual or post-exchange sync, or in plaid-webhook on SYNC_UPDATES_AVAILABLE), the same cash position flow as Teller is used: we invoke the fa-handle-bank-balance-updated edge function with organization_id, bank_account_id, old_balance, new_balance, and optional updated_by. That handler recalculates from all active bank accounts and upserts fa_cash_positions for the current date, so the Cash Allocation & Credit widget stays in sync with Plaid balance changes.

References

  • Spec: specs/fa/specs/FA-PLAID-INTEGRATION-SPEC.md
  • Review: specs/fa/reviews/FA-PLAID-INTEGRATION-REVIEW.md
  • Teller Integration: docs/architecture/integrations/FA_TELLER_INTEGRATION.md
  • Plaid Sandbox verification: docs/testing/PLAID_SANDBOX_VERIFICATION.md (env, payroll $11.11, Transfer UI flow, optional script)
  • Plaid Docs: https://plaid.com/docs/
  • Plaid API Reference: https://plaid.com/docs/api/
  • react-plaid-link: https://github.com/plaid/react-plaid-link