Date: 2026-02-13Documentation Index
Fetch the complete documentation index at: https://docs.encoreos.io/llms.txt
Use this file to discover all available pages before exploring further.
Reference: Plaid Transactions Docs, Add Transactions to your app, Transaction webhooks This document summarizes a deep review of the Encore Health OS Plaid transactions implementation against official Plaid documentation.
Executive Summary
The implementation is largely aligned with Plaid’s Transactions product: Link token includestransactions and days_requested: 730, webhook URL is set, Sync API is used with cursor persistence, and TRANSACTIONS_SYNC_MUTATION_DURING_PAGINATION is handled. One critical gap and several optional improvements were identified.
What’s Done Well
| Requirement (Plaid docs) | Implementation | Status |
|---|---|---|
Link token: products includes transactions | plaid-create-link-token passes products: ['transactions']; createLinkToken uses it. | ✅ |
Link token: webhook for transaction updates | plaid-create-link-token sets webhook: ${supabaseUrl}/functions/v1/plaid-webhook. | ✅ |
Link token: transactions.days_requested | plaid-client.ts: body.transactions = { days_requested: 730 } (max 730). | ✅ |
Exchange public_token → access_token | plaid-exchange-token calls exchangePublicToken(public_token). | ✅ |
| First sync activates SYNC_UPDATES_AVAILABLE | Initial sync in plaid-exchange-token and manual/webhook/scheduled sync all call /transactions/sync. | ✅ |
Save next_cursor for next sync | All sync paths persist plaid_cursor: syncData.next_cursor on fa_bank_accounts. | ✅ |
| TRANSACTIONS_SYNC_MUTATION_DURING_PAGINATION | plaid-sync: on this error, cursor is cleared and sync retried without cursor. | ✅ |
| Webhook: SYNC_UPDATES_AVAILABLE → call sync | plaid-webhook: SYNC_UPDATES_AVAILABLE triggers autoSyncTransactions(). | ✅ |
| Webhook: TRANSACTIONS_REMOVED | plaid-webhook: marks lines by plaid_transaction_id as status: 'removed'. | ✅ |
| Handle added, modified, removed | All sync handlers process added, modified, and removed from Sync response. | ✅ |
| Webhook signature verification | plaid-webhook: JWT verification via Plaid-Verification and body SHA-256. | ✅ |
| Re-auth / connection status | Re-auth errors update plaid_connection_status and plaid_sync_enabled. | ✅ |
| Duplicate institution check | plaid-exchange-token: blocks exchange when plaid_institution_id already exists for org. | ✅ |
Critical Gap: Full Pagination (has_more Loop)
Plaid requirement:If
has_more is true, you must keep calling /transactions/sync with the new next_cursor until has_more is false. Only then persist the final cursor. During pagination, if TRANSACTIONS_SYNC_MUTATION_DURING_PAGINATION occurs, restart from the previous cursor (the one from the first page of the current update), not from the failed page.
Current behavior:We call
/transactions/sync once per sync (manual, webhook, scheduled, or initial). We persist whatever next_cursor and has_more we get and do not loop. So when Plaid returns has_more: true (e.g. 500 items per page), we only ingest the first page and lose the rest until the next webhook/sync.
Impact:
- New connections or large backfills can have hundreds or thousands of transactions; only the first batch (e.g. 100–500) is stored.
- Cursor is advanced as if we had consumed all pages, so the missing pages are never fetched in subsequent incremental syncs.
Implement a while (has_more) loop in all sync paths:
-
_shared/plaid-client.ts- Either keep
syncTransactionsas a single-call helper and add a newsyncTransactionsFull(session)that loops and yields/aggregates pages, or - Have the callers (plaid-sync, plaid-webhook
autoSyncTransactions, plaid-scheduled-sync, plaid-exchange-tokeninitialSync) loop: callsyncTransactions(accessToken, cursor), accumulateadded/modified/removed, setcursor = data.next_cursor, and only exit when!data.has_more. OnTRANSACTIONS_SYNC_MUTATION_DURING_PAGINATION, reset cursor to the cursor at the start of the current loop and restart the loop.
- Either keep
-
Cursor preservation for mutation error:
When starting a pagination run, storecursorBeforeFirstPage = currentCursor. In the loop, usecursor = data.next_cursorfor the next request. If a request returnsTRANSACTIONS_SYNC_MUTATION_DURING_PAGINATION, setcursor = cursorBeforeFirstPageand re-enter the loop (do not persist the failednext_cursor). -
Apply the same loop in:
supabase/functions/plaid-sync/index.tssupabase/functions/plaid-webhook/index.ts(autoSyncTransactions)supabase/functions/plaid-scheduled-sync/index.tssupabase/functions/plaid-exchange-token/index.ts(initialSync)
Optional Improvements
1. Optional: count parameter for /transactions/sync
Plaid supports count (1–500, default 100) per request. We don’t pass it, so we use the default. For large syncs, increasing to 500 could reduce the number of round-trips inside the new pagination loop. Optional and not required for correctness.
2. Optional: Use initial_update_complete / historical_update_complete from webhook
Docs: For SYNC_UPDATES_AVAILABLE, initial_update_complete: true means the first ~30 days are ready; historical_update_complete: true means full history is ready. We don’t read these fields. They could be used to:
- Show UI copy like “Recent transactions ready; full history still loading.”
- Optionally call sync when
initial_update_completeis true to show recent data sooner (docs say this is optional).
SYNC_UPDATES_AVAILABLE, which is correct. Adding handling for these flags would be UX-only.
3. Optional: transactions_update_status from /transactions/sync response
The Sync response includes transactions_update_status (NOT_READY, INITIAL_UPDATE_COMPLETE, HISTORICAL_UPDATE_COMPLETE). We don’t persist it. It could be stored (e.g. on fa_bank_accounts or sync log) for support and UI. Not required for core behavior.
4. Optional: /transactions/refresh (add-on)
Docs: Optional add-on to force an on-demand refresh; then Plaid may fire SYNC_UPDATES_AVAILABLE. We don’t call /transactions/refresh. If you need a “Refresh transactions” button that doesn’t rely on Plaid’s normal schedule, you’d add an edge function that calls this endpoint (subject to Plaid product access).
5. Recurring transactions
Docs:/transactions/recurring/get is an add-on for recurring inflow/outflow streams. We don’t use it. No change needed unless you add that product.
6. Transaction amount sign
Inplaid-client.ts, mapPlaidTransaction comments: “Plaid: positive = outflow (debit), negative = inflow (credit)”. We use amount = Math.abs(txn.amount) and transactionType = txn.amount >= 0 ? 'debit' : 'credit', which matches that convention. No change needed.
Webhook Payload
We handle:webhook_type: 'TRANSACTIONS',webhook_code:SYNC_UPDATES_AVAILABLE,DEFAULT_UPDATE,HISTORICAL_UPDATE,TRANSACTIONS_REMOVEDwebhook_type: 'ITEM',webhook_code:ERROR,PENDING_EXPIRATION,USER_PERMISSION_REVOKED,LOGIN_REPAIRED
SYNC_UPDATES_AVAILABLE is the main webhook; DEFAULT_UPDATE and HISTORICAL_UPDATE are legacy for /transactions/get. Handling them all is harmless and ensures we don’t miss an event.
Testing (Sandbox)
Docs recommend:user_transactions_dynamic(with non-OAuth institution, e.g. First Platypus Bankins_109508) for dynamic data and webhooks./sandbox/transactions/createto simulate new transactions and trigger webhooks.
has_more: true for the first call) to ensure we don’t leave pages unconsumed.
Action Items
| Priority | Action | Owner |
|---|---|---|
| P0 | Implement full pagination (has_more loop) in all four sync call sites; preserve “cursor at start of run” and restart on TRANSACTIONS_SYNC_MUTATION_DURING_PAGINATION. | Engineering |
| P1 | Add unit/integration test for pagination (multi-page sync and mutation-during-pagination restart). | Engineering |
| P2 | Optional: Persist or use transactions_update_status / webhook initial_update_complete / historical_update_complete for UX or support. | Product/Engineering |
| P2 | Optional: Add count: 500 to sync requests if we want fewer round-trips. | Engineering |
References
- Plaid Transactions – Introduction
- Plaid Transactions – Add to your app
- Plaid Transactions – Webhooks
- Plaid API –
/transactions/sync - TRANSACTIONS_SYNC_MUTATION_DURING_PAGINATION
- Internal:
docs/architecture/integrations/PLAID-DOCS-RESEARCH-RECOMMENDATIONS.md,specs/fa/specs/FA-20-plaid-bank-integration.md