Skip to content

Yodlee

Yodlee is the most complex integration - a data aggregator that enables clients to link financial institution accounts for automatic data retrieval.

AttributeValue
Integration TypeData Aggregator
AuthenticationJWT RS512 (25-min token cache)
Data FlowYodlee → RightCapital (inbound)
ComplexityHigh
OwnerIntegration 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:

DimensionStandard IntegrationYodlee
FrameworkSaloon PHPGuzzle HTTP direct
AuthenticationOAuth 2.0JWT RS512 (25-min cache)
InitiatorAdvisor configures connectionClient links bank via FastLink
Data Tablesintegrations + integration_mappingsyodlee_providers + yodlee_provider_accounts + account_yodlees
IntegrationTypeIn enum (47 types)Not in IntegrationType enum (uses AccountSource::YODLEE instead)
Nightly SyncSupported (scheduled)Not participating (refresh requires user session context)
Refresh ModelActive schedulingUser-triggered only
Job QueueLaravel Queue (async)Synchronous (blocks HTTP thread)

The Connector is the transport layer — responsible for authentication, HTTP communication, error mapping, and observability. It does NOT understand business logic.

AspectStandard Saloon ConnectorYodlee Connector
Base classExtends Support\ApiBased\Connectors\ConnectorNone (standalone class)
API styleInstance methods via Saloon Request objectsStatic call(url, method, headers, params, queries, context)
Auth managementSaloon Authenticator + ThreadSafeRefreshAccessTokenInline JWT RS512 generation
Rate limitingSaloon HasRateLimits middleware (Redis)Manual Redis counter (9/120s, silent abort)
Error mappingHasErrorResponseHandler plugin -> standard ExternalServiceException hierarchyCustom exceptions (TokenAuthenticationException, InvalidUserException, etc.)
LoggingSaloon middleware (WriteFatalRequestToFileLog)Manual file write with URL pattern guessing
TracingSaloon APM middleware (auto)Manual OpenTelemetry spans

Code location: retail-api/app/Integrations/Yodlee/Connector.php (~362 lines)

The Integrator is the business orchestration layer — responsible for fetching data via Connector, transforming vendor models to RC models, and persisting to database.

AspectStandard IntegratorYodlee Integrator
Base classExtends IntegrationsCore\Integrators\IntegratorIndependent class, no inheritance
Entry pointsyncAll() / sync(IntegrationMapping)saveProviderAccount()
Data modelIntegration + IntegrationMappingYodleeProviderAccount + AccountYodlee
ResponsibilitiesPure orchestration (fetch + transform + persist)Overloaded: status code mapping (30+ codes -> 5 actions) + data sync + rate limit checking
Entity discoveryOptional EntityProvider interface with cursor paginationNot 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.

  • 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 sub claim)
EnvironmentYodlee CobrandCobrand IDBase URL
Productionrightcapital5010018556https://api.yodlee.com/ysl/
Stagingprivate-rightcapital31910008380https://usyi.stage.api.yodlee.com/ysl/
Developmentrightcapitalllc-stage24520000017https://usyi.stage.api.yodlee.com/ysl/

Configuration: Base URL is set via YODLEE_API_BASE_URL environment variable, referenced in config/yodlee.php.

CategoryCodesDescription
Success4-5Data retrieved successfully
In Progress1-3, 29Sync in progress
PUT Required12, 23, 26-28, 30Credential update needed
Manual Refresh9, 14User must refresh manually
User Action at Bank7-8, 11, 21, 25User must act at institution
Technical Error6, 15-17, 19-20, 22System errors
Unsupported10, 18, 24Account type not supported

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}/:

MethodURI PatternControllerPurpose
GET.../yodlee_provider_accountsYodleeProviderAccountController@indexPull and sync all provider accounts from Yodlee
GET.../yodlee_provider_accounts/{id}YodleeProviderAccountController@showView single provider account (no refresh)
POST.../yodlee_provider_accountsYodleeProviderAccountController@storeCreate/link provider account via provider_account_reference
DELETE.../yodlee_provider_accounts/{id}YodleeProviderAccountController@destroyDelete provider account, cascade force-delete linked accounts
GET.../yodlee_user_tokenYodleeUserToken@indexGet JWT token for FastLink widget
GET.../yodlee_provider_accounts/{id}/refreshYodleeProviderAccountRefreshController@indexPoll refresh status
POST.../yodlee_provider_accounts/{id}/refreshYodleeProviderAccountRefreshController@storeTrigger data refresh (with Redis distributed lock)

Entry points serve three groups of capabilities:

  1. Token acquisition — Frontend gets JWT for FastLink embedded widget
  2. Provider Account CRUD — Manage financial institution connections
  3. Refresh operations — Trigger and poll data refresh
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
  1. Client calls GET /yodlee_user_token → gets JWT for FastLink
  2. FastLink Widget opens in Add Mode → user logs into bank via Yodlee UI
  3. FastLink completes → frontend calls POST /yodlee_provider_accounts with provider_account_reference
  4. Controller calls Api::getProviderAccount() → fetches from Yodlee API
  5. Integrator::saveProviderAccount():
    • Creates/updates YodleeProvider (financial institution metadata, fetched from API if new)
    • Creates/updates YodleeProviderAccount (connection status)
    • If status is SUCCESS/PARTIAL_SUCCESS → calls saveAccountsWhenSuccessful()
    • Api::getAccounts() → fetches all accounts under the provider
    • ProviderAccount::saveAccounts() → creates Account (source=YODLEE) + AccountYodlee satellite + Holdings/Positions
  1. Client calls POST .../refresh (store)
    • lockRefreshAction() → Redis distributed lock (20-min timeout)
    • Api::startProviderAccountRefresh()PUT providerAccounts?providerAccountIds={id}
    • Updates status locally → returns action_required to frontend
  2. Client polls GET .../refresh (index)
    • Api::getProviderAccount()GET providerAccounts/{id}
    • setStatusAndDatasets() → updates local status
    • Response based on status:
      • 200 OK → done, data refreshed
      • 202 Acceptedget_refresh, continue polling
      • 400 Bad Requestput or manual_refresh, user action needed
      • 500 Errorfail, system error
  1. Client/Advisor calls GET /yodlee_provider_accounts (index)
  2. Api::getProviderAccounts() → fetches all provider accounts from Yodlee API
  3. For each provider account:
    • Skips manual accounts (isManual=true)
    • Auto-recreates missing YodleeProviderAccount records
    • Integrator::saveProviderAccount() → updates status + syncs data if successful

Embedded UI component for account linking:

  • Add Mode: Link new accounts
  • Edit Mode: Update credentials (triggered by put action)
  • Refresh Mode: Manual data refresh (triggered by manual_refresh action)

Token is obtained via GET /yodlee_user_tokenApi::getHouseholdToken().

Note: RightCapital does not use FastLink Deep Link Flow (which requires passing providerId to skip bank search). We only pass providerAccountId in Edit/Refresh modes to target specific existing connections.

Important: Yodlee does not participate in nightly sync. Unlike standard integrations, Yodlee uses a user-triggered refresh model — the startProviderAccountRefresh API 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_required to null, no user feedback)

Yodlee provides test sites (“Dag Sites”) for development and testing. These are mock financial institutions with various authentication scenarios.

UsernamePasswordMFA
YodTest2.site19335.1site19335.1None
UsernamePasswordMFA
YodTest.site16441.2site16441.2None
suyantest2.site16441.1site16441.1None
UsernamePasswordMFA
suyantest2.site18769.1site18769.1None
UsernamePasswordMFA
YodTest.site16442.1site16442.1Choose any delivery method, enter: 123456
UsernamePasswordMFA
YodTest.site16486.1site16486.1Q1: w3schools, Q2: Texas
suyantest2.site16486.1site16486.1State: Texas, School: w3schools
UsernamePasswordMFA
suyantest2.site16445.2site16445.2Token: 123456

Note: These are Yodlee’s test sites (Dag Sites) for development. Do not use in production.

The Integrator maps 30+ status codes into 5 action categories that drive frontend behavior:

ActionMeaningFrontend Behavior
done (null)Data retrieved successfullyShow data, no action
get_refreshSync in progressContinue polling GET .../refresh
putCredentials expired / consent issuesOpen FastLink Edit flow
manual_refreshMFA verification neededOpen FastLink Refresh flow
failUnrecoverable 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():

  1. SUCCESS → done
  2. IN_PROGRESS / LOGIN_IN_PROGRESS / USER_INPUT_REQUIRED → get_refresh
  3. PARTIAL_SUCCESS / FAILED → check each dataset’s additionalStatus for specific action
  4. If get_refresh but rate limit hit → abort
ComponentPath
Provider Account CRUDretail-api/app/Http/Controllers/Advisors/Households/YodleeProviderAccountController.php
Refresh Controllerretail-api/app/Http/Controllers/Advisors/Households/YodleeProviderAccountRefreshController.php
User Tokenretail-api/app/Http/Controllers/Advisors/Households/YodleeUserToken.php
Routesretail-api/routes/web.php (lines 181-189)
ComponentLinesPath
Api.php~488retail-api/app/Integrations/Yodlee/Api.php
Connector.php~362retail-api/app/Integrations/Yodlee/Connector.php
Integrator.php~592retail-api/app/Integrations/Yodlee/Integrator.php
ComponentPath
YodleeProviderAccountretail-api/app/Models/YodleeProviderAccount.php
YodleeProviderretail-api/app/Models/YodleeProvider.php
AccountYodleeretail-api/app/Models/AccountYodlee.php
Eventsretail-api/app/Models/Events/AccountYodleeEvent.php, YodleeProviderAccountEvent.php
Policiesretail-api/app/Models/Policies/YodleeProviderAccountPolicy.php, YodleeProviderPolicy.php
HTTP Resourcesretail-api/app/Http/Resources/YodleeProviderAccountResource.php, YodleeProviderResource.php, AccountYodleeResource.php
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.php

Symptom: User repeatedly asked for MFA

Solution: Add RightCapital as trusted device at bank

Symptom: SITE_CHANGED status

Cause: Bank updated login page, Yodlee needs to update connector

Solution: Wait for Yodlee fix (may take days)

Symptom: Auth errors during long syncs

Solution: Token refresh handled automatically; may need reconnect

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.

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.

  1. Synchronous blocking — All operations block HTTP threads, no background jobs
  2. No nightly sync — Passive trigger model, user session required
  3. 20-min lock timeout — Too long, masks stuck processes
  4. Silent rate limiting — Users get no feedback when hitting the 9-refreshes/120s limit; system silently aborts with action_required: null
  5. Large Integrator classIntegrator.php handles status mapping, account saving, transaction processing, and rate limiting in a single 592-line class