Skip to content

API-based Vendor Specifics

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

Complexity Vendor Reason
High Yodlee Legacy aggregator, 30+ status codes, complex state machine
High Schwab API OAuth 2.0, large data volumes, strict rate limits
Medium Orion OAuth, multi-entity types, portfolio management
Medium Addepar OAuth, complex account hierarchies
Medium Wealthbox CRM integration, many entity types
Low Redtail Simple API Key, straightforward endpoints
Low Riskalyze Simple 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.

Feature Implementation
Authentication JWT RS512 (25-min token cache)
Sync Mode Passive refresh (user-triggered only)
Status Codes 30+ codes mapped to 5 action categories
Rate Limit 9 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:

Endpoint Rate Limit
Accounts 120 req/min
Positions 120 req/min
Transactions 60 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 Entity Maps To
Client Household
Account Account
Portfolio Account group
Model TargetCategoryMix
  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 Entity Maps To
Contact Person
Household Household
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:

Developer Integrations
Qianwei Hao LPL, Black Diamond, Redtail, Asset Book, Asset Mark, Fidelity, First Clearing, Folio Investing, Morningstar Advisor
Kewei Yan Betterment, Investigo, Advyzon, Orion, Allianz API, Wealthbox, My529, Pershing, Trust America
Tingsong Xu Albridge, Flourish, Apex, Blueleaf, Circle Black, Commonwealth, Panoramix, Riskalyze, Tamarac
Yan Hu Allianz, 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();