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.

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)

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)

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.

  • 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)
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.

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

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:

  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.

Username Password MFA
YodTest2.site19335.1 site19335.1 None
Username Password MFA
YodTest.site16441.2 site16441.2 None
suyantest2.site16441.1 site16441.1 None
Username Password MFA
suyantest2.site18769.1 site18769.1 None
Username Password MFA
YodTest.site16442.1 site16442.1 Choose any delivery method, enter: 123456
Username Password MFA
YodTest.site16486.1 site16486.1 Q1: w3schools, Q2: Texas
suyantest2.site16486.1 site16486.1 State: Texas, School: w3schools
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.

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():

  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
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)
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
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
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