Skip to content

API-based Vendor Specifics

This document covers vendor-specific implementation details, quirks, and troubleshooting information for major API-based integrations.

ComplexityVendorReason
HighYodleeLegacy aggregator, 30+ status codes, complex state machine
HighSchwab APIOAuth 2.0, large data volumes, strict rate limits
MediumOrionOAuth, multi-entity types, portfolio management
MediumAddeparOAuth, complex account hierarchies
MediumWealthboxCRM integration, many entity types
LowRedtailSimple API Key, straightforward endpoints
LowRiskalyzeSimple API, questionnaire data

Location: app/Integrations/Yodlee/

Yodlee is the most complex integration due to its legacy aggregator nature and extensive status code system. It uses a separate architecture from the standard Saloon-based integrations.

Full Documentation: See Yodlee Vendor Documentation for comprehensive architecture details, data flow diagrams, status codes, and request entry points.

FeatureImplementation
AuthenticationJWT RS512 (25-min token cache)
Sync ModePassive refresh (user-triggered only)
Status Codes30+ codes mapped to 5 action categories
Rate Limit9 refreshes per 120 seconds
  1. Token expiration during long syncs — JWT tokens expire after 25 min; token is cached in Redis and auto-refreshed
  2. MFA loops — Some banks require MFA on every access; users may need to add RC as trusted device
  3. Site changes — When banks update their login pages, Yodlee needs to update their connectors
  4. Synchronous blocking — All refresh operations block HTTP threads; no queue/async processing (see DEV-3438)

Location: app/Integrations/SchwabApi/

OAuth 2.0 integration with Charles Schwab’s official API.

OAuth 2.0 Authorization Code flow with PKCE.

OAuth2/Provider.php
class Provider extends AbstractProvider
{
public function getBaseAuthorizationUrl(): string
{
return 'https://api.schwab.com/oauth/authorize';
}
public function getBaseAccessTokenUrl(array $params): string
{
return 'https://api.schwab.com/oauth/token';
}
protected function getDefaultScopes(): array
{
return ['accounts', 'positions', 'transactions'];
}
}

Schwab returns large datasets with cursor-based pagination:

public function listEntities(array $conditions, ?string $cursor = null): CursorPage
{
$response = $this->connector->call('/accounts', [
'pageToken' => $cursor,
'pageSize' => 100,
]);
return new CursorPage(
data: $this->mapAccounts($response['accounts']),
next_cursor: $response['nextPageToken'] ?? null
);
}

Schwab enforces strict rate limits:

EndpointRate Limit
Accounts120 req/min
Positions120 req/min
Transactions60 req/min
// Handle rate limiting
public function call(string $endpoint): array
{
try {
return $this->http->get($endpoint)->json();
} catch (TooManyRequestException $e) {
$retryAfter = $e->response->header('Retry-After') ?? 60;
throw new TooManyRequestException($retryAfter);
}
}
  1. Token refresh race condition - Multiple concurrent requests may try to refresh token simultaneously; use ThreadSafeOAuthTokenManager
  2. Weekend data delays - Transaction data may be delayed on weekends
  3. Account type mapping - Schwab has many account types (IRA, Roth, 401k, etc.) that need careful mapping

Location: app/Integrations/Orion/

OAuth integration with Orion Portfolio Solutions.

OAuth 2.0 with long-lived refresh tokens.

Orion EntityMaps To
ClientHousehold
AccountAccount
PortfolioAccount group
ModelTargetCategoryMix
  1. Hierarchical accounts - Orion has nested account structures that need flattening
  2. Model assignment sync - Keeping portfolio models in sync with RC target allocations

Location: app/Integrations/Wealthbox/

CRM integration with Wealthbox.

API Key in header:

protected function getAuthHeaders(): array
{
return [
'ACCESS_TOKEN' => $this->integration->credentials['api_key'],
];
}
Wealthbox EntityMaps To
ContactPerson
HouseholdHousehold
Opportunity(not mapped)
Task(not mapped)

Wealthbox supports household tags for categorization:

class Integrator implements SupportsHouseholdTags
{
public function listTags(): array
{
return $this->connector->call('/tags');
}
}

Location: app/Integrations/Redtail/

CRM integration with Redtail Technology.

API Key + User Key:

protected function getAuthHeaders(): array
{
return [
'Authorization' => 'Userkeyauth ' . $this->integration->credentials['user_key'],
'ApiKey' => $this->integration->credentials['api_key'],
];
}
  1. Contact deduplication - Redtail may have duplicate contacts that need merging logic
  2. Custom fields - Redtail has extensive custom field support that may not map cleanly

Contact these developers for integration-specific questions:

DeveloperIntegrations
Qianwei HaoLPL, Black Diamond, Redtail, Asset Book, Asset Mark, Fidelity, First Clearing, Folio Investing, Morningstar Advisor
Kewei YanBetterment, Investigo, Advyzon, Orion, Allianz API, Wealthbox, My529, Pershing, Trust America
Tingsong XuAlbridge, Flourish, Apex, Blueleaf, Circle Black, Commonwealth, Panoramix, Riskalyze, Tamarac
Yan HuAllianz, Altruist, Bridge FT, Capitect, DST, FinFolio, RBC, Schwab (file), Schwab API, Wealth Access
Winston Li (Zefeng Li)Addepar, Interactive Brokers, Jackson, Max My Interest, Morningstar Office, Nationwide, Pacific Life, Raymond James, SEI, Smart Office
// In Connector, log all requests
public function call(string $endpoint): array
{
$response = $this->http->get($endpoint);
Log::channel('integrations')->debug('API Request', [
'integration_id' => $this->integration->id,
'endpoint' => $endpoint,
'status' => $response->status(),
'body' => $response->json(),
]);
return $response->json();
}
-- Find failing integrations
SELECT id, type, failed_since, failed_biz_days, failure_message
FROM integrations
WHERE failed_since IS NOT NULL
ORDER BY failed_biz_days DESC;
-- Check recent sync activity
SELECT im.id, im.reference, im.sync_status, im.last_completed_at
FROM integration_mappings im
JOIN integrations i ON im.integration_id = i.id
WHERE i.type = 'YODLEE'
ORDER BY im.last_completed_at DESC
LIMIT 20;
// Verify credentials without full sync
$connector = new Connector($integration);
$valid = $connector->verifyIntegrationCredentials();