Yodlee
Yodlee is the most complex integration - a data aggregator that enables clients to link financial institution accounts for automatic data retrieval.
Overview
Section titled “Overview”| Attribute | Value |
|---|---|
| Integration Type | Data Aggregator |
| Authentication | JWT RS512 (25-min token cache) |
| Data Flow | Yodlee → RightCapital (inbound) |
| Complexity | High |
| Owner | Integration Team (shared) |
Architecture Differences from Standard Integrations
Section titled “Architecture Differences from Standard Integrations”Yodlee is a completely separate legacy system from the standard Saloon-based integration architecture:
| Dimension | Standard Integration | Yodlee |
|---|---|---|
| Framework | Saloon PHP | Guzzle HTTP direct |
| Authentication | OAuth 2.0 | JWT RS512 (25-min cache) |
| Initiator | Advisor configures connection | Client links bank via FastLink |
| Data Tables | integrations + integration_mappings | yodlee_providers + yodlee_provider_accounts + account_yodlees |
| IntegrationType | In enum (47 types) | Not in IntegrationType enum (uses AccountSource::YODLEE instead) |
| Nightly Sync | Supported (scheduled) | Not participating (refresh requires user session context) |
| Refresh Model | Active scheduling | User-triggered only |
| Job Queue | Laravel Queue (async) | Synchronous (blocks HTTP thread) |
Connector Layer Comparison
Section titled “Connector Layer Comparison”The Connector is the transport layer — responsible for authentication, HTTP communication, error mapping, and observability. It does NOT understand business logic.
| Aspect | Standard Saloon Connector | Yodlee Connector |
|---|---|---|
| Base class | Extends Support\ApiBased\Connectors\Connector | None (standalone class) |
| API style | Instance methods via Saloon Request objects | Static call(url, method, headers, params, queries, context) |
| Auth management | Saloon Authenticator + ThreadSafeRefreshAccessToken | Inline JWT RS512 generation |
| Rate limiting | Saloon HasRateLimits middleware (Redis) | Manual Redis counter (9/120s, silent abort) |
| Error mapping | HasErrorResponseHandler plugin -> standard ExternalServiceException hierarchy | Custom exceptions (TokenAuthenticationException, InvalidUserException, etc.) |
| Logging | Saloon middleware (WriteFatalRequestToFileLog) | Manual file write with URL pattern guessing |
| Tracing | Saloon APM middleware (auto) | Manual OpenTelemetry spans |
Code location: retail-api/app/Integrations/Yodlee/Connector.php (~362 lines)
Integrator Layer Comparison
Section titled “Integrator Layer Comparison”The Integrator is the business orchestration layer — responsible for fetching data via Connector, transforming vendor models to RC models, and persisting to database.
| Aspect | Standard Integrator | Yodlee Integrator |
|---|---|---|
| Base class | Extends IntegrationsCore\Integrators\Integrator | Independent class, no inheritance |
| Entry point | syncAll() / sync(IntegrationMapping) | saveProviderAccount() |
| Data model | Integration + IntegrationMapping | YodleeProviderAccount + AccountYodlee |
| Responsibilities | Pure orchestration (fetch + transform + persist) | Overloaded: status code mapping (30+ codes -> 5 actions) + data sync + rate limit checking |
| Entity discovery | Optional EntityProvider interface with cursor pagination | Not applicable (FastLink handles entity selection) |
Code location: retail-api/app/Integrations/Yodlee/Integrator.php (~592 lines)
Technical debt: Yodlee’s Integrator handles too many concerns in a single class. The status code mapping logic, rate limit checking, and data sync orchestration should ideally be separated. See Yodlee Soft-Refresh Optimization for the planned architectural improvements.
Authentication
Section titled “Authentication”- Algorithm: RS512 (RSA + SHA-512)
- Token TTL: 25 minutes (
TOKEN_EXPIRE_SECONDS = 1_500)- Yodlee official TTL: 30 minutes
- RC sets 25 minutes as safety margin to prevent expiration during long operations
- Per-Household: Each household has unique Yodlee user via
loginName - Admin Token: For user registration (no
subclaim)
API Endpoints
Section titled “API Endpoints”| Environment | Yodlee Cobrand | Cobrand ID | Base URL |
|---|---|---|---|
| Production | rightcapital | 5010018556 | https://api.yodlee.com/ysl/ |
| Staging | private-rightcapital | 31910008380 | https://usyi.stage.api.yodlee.com/ysl/ |
| Development | rightcapitalllc-stage | 24520000017 | https://usyi.stage.api.yodlee.com/ysl/ |
Configuration: Base URL is set via YODLEE_API_BASE_URL environment variable, referenced in config/yodlee.php.
Status Code System (30+)
Section titled “Status Code System (30+)”| Category | Codes | Description |
|---|---|---|
| Success | 4-5 | Data retrieved successfully |
| In Progress | 1-3, 29 | Sync in progress |
| PUT Required | 12, 23, 26-28, 30 | Credential update needed |
| Manual Refresh | 9, 14 | User must refresh manually |
| User Action at Bank | 7-8, 11, 21, 25 | User must act at institution |
| Technical Error | 6, 15-17, 19-20, 22 | System errors |
| Unsupported | 10, 18, 24 | Account type not supported |
Request Entry Points
Section titled “Request Entry Points”All Yodlee routes are defined in retail-api/routes/web.php, gated by feature:FEATURE_ACCOUNT_AGGREGATION middleware (all routes require this feature flag to be enabled for the advisor), nested under advisors/{advisor}/households/{household}/:
| Method | URI Pattern | Controller | Purpose |
|---|---|---|---|
| GET | .../yodlee_provider_accounts | YodleeProviderAccountController@index | Pull and sync all provider accounts from Yodlee |
| GET | .../yodlee_provider_accounts/{id} | YodleeProviderAccountController@show | View single provider account (no refresh) |
| POST | .../yodlee_provider_accounts | YodleeProviderAccountController@store | Create/link provider account via provider_account_reference |
| DELETE | .../yodlee_provider_accounts/{id} | YodleeProviderAccountController@destroy | Delete provider account, cascade force-delete linked accounts |
| GET | .../yodlee_user_token | YodleeUserToken@index | Get JWT token for FastLink widget |
| GET | .../yodlee_provider_accounts/{id}/refresh | YodleeProviderAccountRefreshController@index | Poll refresh status |
| POST | .../yodlee_provider_accounts/{id}/refresh | YodleeProviderAccountRefreshController@store | Trigger data refresh (with Redis distributed lock) |
Entry points serve three groups of capabilities:
- Token acquisition — Frontend gets JWT for FastLink embedded widget
- Provider Account CRUD — Manage financial institution connections
- Refresh operations — Trigger and poll data refresh
Data Flow
Section titled “Data Flow”Overall Flow
Section titled “Overall Flow”flowchart TD
subgraph "Client Portal (Frontend)"
T[GET /yodlee_user_token]
FL[FastLink Widget]
PA[POST /yodlee_provider_accounts]
RF[POST .../refresh]
PL[GET .../refresh]
end
subgraph "Controller Layer"
UTC[YodleeUserToken]
PAC[YodleeProviderAccountController]
RFC[YodleeProviderAccountRefreshController]
end
subgraph "Integration Layer"
API[Api.php - HTTP Client]
INT[Integrator.php - Business Logic]
end
subgraph "Yodlee External"
YA[Yodlee REST API]
end
subgraph "Database"
YP[yodlee_providers]
YPA[yodlee_provider_accounts]
AY[account_yodlees]
ACC[accounts]
POS[positions]
TXN[transactions]
end
T --> UTC --> API
FL -->|OAuth| YA
PA --> PAC --> INT
RF --> RFC -->|Lock + Refresh| API
PL --> RFC -->|Poll Status| API
INT --> API --> YA
INT -->|saveProviderAccount| YP & YPA
INT -->|saveAccountsWhenSuccessful| AY & ACC
ACC --> POS
ACC --> TXN
Scenario 1: First-time Account Linking
Section titled “Scenario 1: First-time Account Linking”- Client calls
GET /yodlee_user_token→ gets JWT for FastLink - FastLink Widget opens in Add Mode → user logs into bank via Yodlee UI
- FastLink completes → frontend calls
POST /yodlee_provider_accountswithprovider_account_reference - Controller calls
Api::getProviderAccount()→ fetches from Yodlee API Integrator::saveProviderAccount():- Creates/updates
YodleeProvider(financial institution metadata, fetched from API if new) - Creates/updates
YodleeProviderAccount(connection status) - If status is
SUCCESS/PARTIAL_SUCCESS→ callssaveAccountsWhenSuccessful() Api::getAccounts()→ fetches all accounts under the providerProviderAccount::saveAccounts()→ createsAccount(source=YODLEE) +AccountYodleesatellite + Holdings/Positions
- Creates/updates
Scenario 2: Manual Refresh
Section titled “Scenario 2: Manual Refresh”- Client calls
POST .../refresh(store)lockRefreshAction()→ Redis distributed lock (20-min timeout)Api::startProviderAccountRefresh()→PUT providerAccounts?providerAccountIds={id}- Updates status locally → returns
action_requiredto frontend
- Client polls
GET .../refresh(index)Api::getProviderAccount()→GET providerAccounts/{id}setStatusAndDatasets()→ updates local status- Response based on status:
200 OK→ done, data refreshed202 Accepted→get_refresh, continue polling400 Bad Request→putormanual_refresh, user action needed500 Error→fail, system error
Scenario 3: List All Linked Accounts
Section titled “Scenario 3: List All Linked Accounts”- Client/Advisor calls
GET /yodlee_provider_accounts(index) Api::getProviderAccounts()→ fetches all provider accounts from Yodlee API- For each provider account:
- Skips manual accounts (
isManual=true) - Auto-recreates missing
YodleeProviderAccountrecords Integrator::saveProviderAccount()→ updates status + syncs data if successful
- Skips manual accounts (
FastLink Widget
Section titled “FastLink Widget”Embedded UI component for account linking:
- Add Mode: Link new accounts
- Edit Mode: Update credentials (triggered by
putaction) - Refresh Mode: Manual data refresh (triggered by
manual_refreshaction)
Token is obtained via GET /yodlee_user_token → Api::getHouseholdToken().
Note: RightCapital does not use FastLink Deep Link Flow (which requires passing
providerIdto skip bank search). We only passproviderAccountIdin Edit/Refresh modes to target specific existing connections.
Nightly Sync
Section titled “Nightly Sync”Important: Yodlee does not participate in nightly sync. Unlike standard integrations, Yodlee uses a user-triggered refresh model — the
startProviderAccountRefreshAPI requires an active user session context, making scheduled background sync impossible.
Transaction date range:
- New accounts (no prior transactions): 6 months
- Existing accounts (with prior transactions): 2-week overlap from most recent transaction
- Employee impersonation mode: 3 months (debugging/support use case)
Rate limit:
- Maximum: 9 refreshes per 120 seconds (tracked via Redis counter per provider account)
- Mechanism: Cumulative counter within a 120-second sliding window (not enforced interval between requests)
- Behavior on exceed: Silent abort (sets
action_requiredtonull, no user feedback)
Test Accounts (Staging/Dev)
Section titled “Test Accounts (Staging/Dev)”Yodlee provides test sites (“Dag Sites”) for development and testing. These are mock financial institutions with various authentication scenarios.
OAuth Flow
Section titled “OAuth Flow”| Username | Password | MFA |
|---|---|---|
| YodTest2.site19335.1 | site19335.1 | None |
Basic Login
Section titled “Basic Login”| Username | Password | MFA |
|---|---|---|
| YodTest.site16441.2 | site16441.2 | None |
| suyantest2.site16441.1 | site16441.1 | None |
CAPTCHA
Section titled “CAPTCHA”| Username | Password | MFA |
|---|---|---|
| suyantest2.site18769.1 | site18769.1 | None |
Multi-Level (OTP)
Section titled “Multi-Level (OTP)”| Username | Password | MFA |
|---|---|---|
| YodTest.site16442.1 | site16442.1 | Choose any delivery method, enter: 123456 |
Security Questions (Challenge/Response)
Section titled “Security Questions (Challenge/Response)”| Username | Password | MFA |
|---|---|---|
| YodTest.site16486.1 | site16486.1 | Q1: w3schools, Q2: Texas |
| suyantest2.site16486.1 | site16486.1 | State: Texas, School: w3schools |
Token-based MFA
Section titled “Token-based MFA”| Username | Password | MFA |
|---|---|---|
| suyantest2.site16445.2 | site16445.2 | Token: 123456 |
Note: These are Yodlee’s test sites (Dag Sites) for development. Do not use in production.
Status Action Decision
Section titled “Status Action Decision”The Integrator maps 30+ status codes into 5 action categories that drive frontend behavior:
| Action | Meaning | Frontend Behavior |
|---|---|---|
done (null) | Data retrieved successfully | Show data, no action |
get_refresh | Sync in progress | Continue polling GET .../refresh |
put | Credentials expired / consent issues | Open FastLink Edit flow |
manual_refresh | MFA verification needed | Open FastLink Refresh flow |
fail | Unrecoverable error (site down, tech error) | Show error message |
abort (null) | Rate limit exceeded (>9 refreshes/120s) | Silent stop, no error shown |
Decision hierarchy in getActionRequiredFromStatusAndDatasetAdditionalStatus():
SUCCESS→ doneIN_PROGRESS/LOGIN_IN_PROGRESS/USER_INPUT_REQUIRED→ get_refreshPARTIAL_SUCCESS/FAILED→ check each dataset’sadditionalStatusfor specific action- If
get_refreshbut rate limit hit → abort
Code Locations
Section titled “Code Locations”Controllers
Section titled “Controllers”| Component | Path |
|---|---|
| Provider Account CRUD | retail-api/app/Http/Controllers/Advisors/Households/YodleeProviderAccountController.php |
| Refresh Controller | retail-api/app/Http/Controllers/Advisors/Households/YodleeProviderAccountRefreshController.php |
| User Token | retail-api/app/Http/Controllers/Advisors/Households/YodleeUserToken.php |
| Routes | retail-api/routes/web.php (lines 181-189) |
Integration Layer
Section titled “Integration Layer”| Component | Lines | Path |
|---|---|---|
| Api.php | ~488 | retail-api/app/Integrations/Yodlee/Api.php |
| Connector.php | ~362 | retail-api/app/Integrations/Yodlee/Connector.php |
| Integrator.php | ~592 | retail-api/app/Integrations/Yodlee/Integrator.php |
Models & Resources
Section titled “Models & Resources”| Component | Path |
|---|---|
| YodleeProviderAccount | retail-api/app/Models/YodleeProviderAccount.php |
| YodleeProvider | retail-api/app/Models/YodleeProvider.php |
| AccountYodlee | retail-api/app/Models/AccountYodlee.php |
| Events | retail-api/app/Models/Events/AccountYodleeEvent.php, YodleeProviderAccountEvent.php |
| Policies | retail-api/app/Models/Policies/YodleeProviderAccountPolicy.php, YodleeProviderPolicy.php |
| HTTP Resources | retail-api/app/Http/Resources/YodleeProviderAccountResource.php, YodleeProviderResource.php, AccountYodleeResource.php |
Vendor Models (DTOs)
Section titled “Vendor Models (DTOs)”retail-api/app/Integrations/Yodlee/├── Api.php # Yodlee REST API client (30+ methods)├── Connector.php # HTTP transport, JWT auth├── Integrator.php # Business logic, status mapping, rate limiting├── Exceptions/ # 7 exception types│ ├── Exception.php, InvalidArgumentException.php│ ├── InvalidUserException.php, NotFoundException.php│ ├── RequestException.php, TokenAuthenticationException.php│ └── UpdateNotAllowedException.php└── Models/ # Yodlee API response DTOs ├── Model.php, Account.php, BankAccount.php ├── CardAccount.php, Holding.php, InvestmentAccount.php ├── Loan.php, LoanAccount.php, ProviderAccount.php └── Transaction.phpCommon Issues
Section titled “Common Issues”MFA Loops
Section titled “MFA Loops”Symptom: User repeatedly asked for MFA
Solution: Add RightCapital as trusted device at bank
Site Changes
Section titled “Site Changes”Symptom: SITE_CHANGED status
Cause: Bank updated login page, Yodlee needs to update connector
Solution: Wait for Yodlee fix (may take days)
Token Expiration
Section titled “Token Expiration”Symptom: Auth errors during long syncs
Solution: Token refresh handled automatically; may need reconnect
Rate Limiting
Section titled “Rate Limiting”Symptom: 429 Too Many Requests or silent refresh abort
Cause: Exceeded 9 refreshes within a 120-second sliding window (tracked via Redis counter per provider account)
Solution: Wait and retry. Note: when the internal rate limit is hit, the system silently sets action_required to null (abort) — the user gets no error feedback.
Synchronous Blocking (DEV-3438)
Section titled “Synchronous Blocking (DEV-3438)”Symptom: Refresh times out, manual refresh button not visible
Cause: All refresh operations are synchronous, blocking HTTP request threads. The 20-minute Redis lock timeout can mask stuck processes.
Solution: Under investigation — potential approaches include Laravel Queue + polling, Yodlee webhook integration, or a hybrid approach. See DEV-3438.
Known Technical Debt
Section titled “Known Technical Debt”- Synchronous blocking — All operations block HTTP threads, no background jobs
- No nightly sync — Passive trigger model, user session required
- 20-min lock timeout — Too long, masks stuck processes
- Silent rate limiting — Users get no feedback when hitting the 9-refreshes/120s limit; system silently aborts with
action_required: null - Large Integrator class —
Integrator.phphandles status mapping, account saving, transaction processing, and rate limiting in a single 592-line class
Related
Section titled “Related”- Vendor Specifics - Detailed technical implementation
- Data Models - Yodlee table schemas and ER diagrams
- API Integrations - Yodlee vs standard architecture comparison
- DEV-3438 - Async refresh investigation
- Soft-Refresh Optimization - Architectural decision for async conversion and Saloon migration
Sources
Section titled “Sources”- Retail API changes for Yodlee upgrade to V1.1 - Notion documentation
- Source code analysis:
retail-api/routes/web.php,app/Http/Controllers/Advisors/Households/Yodlee*.php,app/Integrations/Yodlee/