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.

FileLinesPurpose
Api.php~36830+ API methods for Yodlee REST API
Connector.php~372JWT authentication, API calls
Integrator.php~1100+Complex state machine, sync logic
// JWT-based authentication
class Connector
{
public function call(string $endpoint): array
{
$jwt = $this->generateJwt();
return Http::withToken($jwt)
->get($this->baseUrl . $endpoint)
->json();
}
private function generateJwt(): string
{
return JWT::encode([
'iss' => $this->credentials['issuer'],
'sub' => $this->credentials['user_id'],
'iat' => time(),
'exp' => time() + 3600,
], $this->privateKey, 'RS256');
}
}

Yodlee returns 30+ status codes indicating account state. These map to user actions:

Status CodeCategoryUser Action Required
SUCCESSOKNone
PARTIAL_DATAOKNone (some data unavailable)
LOGIN_REQUIREDAuthRe-enter credentials
INVALID_CREDENTIALSAuthFix credentials
MFA_REQUIREDAuthComplete 2FA
SECURITY_QUESTIONAuthAnswer security question
TOKEN_EXPIREDAuthReconnect account
ACCOUNT_LOCKEDBlockUnlock at bank
SITE_UNAVAILABLETempWait and retry
SITE_CHANGEDBlockWait for Yodlee fix
DATA_NOT_AVAILABLEBlockData not supported
// Simplified from Integrator.php
public function mapStatusToAction(string $status): array
{
return match($status) {
'SUCCESS', 'PARTIAL_DATA' => [
'action' => 'none',
'can_sync' => true,
],
'LOGIN_REQUIRED', 'INVALID_CREDENTIALS' => [
'action' => 'edit_credentials',
'can_sync' => false,
'message' => 'Please update your login credentials',
],
'MFA_REQUIRED', 'SECURITY_QUESTION' => [
'action' => 'refresh',
'can_sync' => false,
'message' => 'Additional verification required',
],
'SITE_UNAVAILABLE' => [
'action' => 'retry_later',
'can_sync' => false,
'message' => 'Bank website temporarily unavailable',
],
default => [
'action' => 'contact_support',
'can_sync' => false,
],
};
}
  1. Token expiration during long syncs - JWT tokens expire after 1 hour; long syncs may need token refresh mid-process
  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

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