API-based Vendor Specifics
This document covers vendor-specific implementation details, quirks, and troubleshooting information for major API-based integrations.
Complexity Classification
Section titled “Complexity Classification”| 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 |
Yodlee (Data Aggregator)
Section titled “Yodlee (Data Aggregator)”Location: app/Integrations/Yodlee/
Yodlee is the most complex integration due to its legacy aggregator nature and extensive status code system.
Files Overview
Section titled “Files Overview”| File | Lines | Purpose |
|---|---|---|
Api.php | ~368 | 30+ API methods for Yodlee REST API |
Connector.php | ~372 | JWT authentication, API calls |
Integrator.php | ~1100+ | Complex state machine, sync logic |
Authentication
Section titled “Authentication”// JWT-based authenticationclass 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'); }}Status Code System
Section titled “Status Code System”Yodlee returns 30+ status codes indicating account state. These map to user actions:
| Status Code | Category | User Action Required |
|---|---|---|
SUCCESS | OK | None |
PARTIAL_DATA | OK | None (some data unavailable) |
LOGIN_REQUIRED | Auth | Re-enter credentials |
INVALID_CREDENTIALS | Auth | Fix credentials |
MFA_REQUIRED | Auth | Complete 2FA |
SECURITY_QUESTION | Auth | Answer security question |
TOKEN_EXPIRED | Auth | Reconnect account |
ACCOUNT_LOCKED | Block | Unlock at bank |
SITE_UNAVAILABLE | Temp | Wait and retry |
SITE_CHANGED | Block | Wait for Yodlee fix |
DATA_NOT_AVAILABLE | Block | Data not supported |
Status Mapping Logic
Section titled “Status Mapping Logic”// Simplified from Integrator.phppublic 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, ], };}Common Issues
Section titled “Common Issues”- Token expiration during long syncs - JWT tokens expire after 1 hour; long syncs may need token refresh mid-process
- MFA loops - Some banks require MFA on every access; users may need to add RC as trusted device
- Site changes - When banks update their login pages, Yodlee needs to update their connectors
Schwab API
Section titled “Schwab API”Location: app/Integrations/SchwabApi/
OAuth 2.0 integration with Charles Schwab’s official API.
Authentication
Section titled “Authentication”OAuth 2.0 Authorization Code flow with PKCE.
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']; }}Pagination Handling
Section titled “Pagination Handling”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 );}Rate Limiting
Section titled “Rate Limiting”Schwab enforces strict rate limits:
| Endpoint | Rate Limit |
|---|---|
| Accounts | 120 req/min |
| Positions | 120 req/min |
| Transactions | 60 req/min |
// Handle rate limitingpublic 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); }}Common Issues
Section titled “Common Issues”- Token refresh race condition - Multiple concurrent requests may try to refresh token simultaneously; use
ThreadSafeOAuthTokenManager - Weekend data delays - Transaction data may be delayed on weekends
- 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.
Authentication
Section titled “Authentication”OAuth 2.0 with long-lived refresh tokens.
Key Entities
Section titled “Key Entities”| Orion Entity | Maps To |
|---|---|
| Client | Household |
| Account | Account |
| Portfolio | Account group |
| Model | TargetCategoryMix |
Common Issues
Section titled “Common Issues”- Hierarchical accounts - Orion has nested account structures that need flattening
- Model assignment sync - Keeping portfolio models in sync with RC target allocations
Wealthbox
Section titled “Wealthbox”Location: app/Integrations/Wealthbox/
CRM integration with Wealthbox.
Authentication
Section titled “Authentication”API Key in header:
protected function getAuthHeaders(): array{ return [ 'ACCESS_TOKEN' => $this->integration->credentials['api_key'], ];}Key Entities
Section titled “Key Entities”| Wealthbox Entity | Maps To |
|---|---|
| Contact | Person |
| Household | Household |
| Opportunity | (not mapped) |
| Task | (not mapped) |
Tags Support
Section titled “Tags Support”Wealthbox supports household tags for categorization:
class Integrator implements SupportsHouseholdTags{ public function listTags(): array { return $this->connector->call('/tags'); }}Redtail
Section titled “Redtail”Location: app/Integrations/Redtail/
CRM integration with Redtail Technology.
Authentication
Section titled “Authentication”API Key + User Key:
protected function getAuthHeaders(): array{ return [ 'Authorization' => 'Userkeyauth ' . $this->integration->credentials['user_key'], 'ApiKey' => $this->integration->credentials['api_key'], ];}Common Issues
Section titled “Common Issues”- Contact deduplication - Redtail may have duplicate contacts that need merging logic
- Custom fields - Redtail has extensive custom field support that may not map cleanly
Developer Ownership
Section titled “Developer Ownership”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 |
Debugging Tips
Section titled “Debugging Tips”Enable Request Logging
Section titled “Enable Request Logging”// In Connector, log all requestspublic 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();}Check Integration State
Section titled “Check Integration State”-- Find failing integrationsSELECT id, type, failed_since, failed_biz_days, failure_messageFROM integrationsWHERE failed_since IS NOT NULLORDER BY failed_biz_days DESC;
-- Check recent sync activitySELECT im.id, im.reference, im.sync_status, im.last_completed_atFROM integration_mappings imJOIN integrations i ON im.integration_id = i.idWHERE i.type = 'YODLEE'ORDER BY im.last_completed_at DESCLIMIT 20;Test Credentials
Section titled “Test Credentials”// Verify credentials without full sync$connector = new Connector($integration);$valid = $connector->verifyIntegrationCredentials();Related Documentation
Section titled “Related Documentation”- Patterns - Connector and Integrator design
- Sync Lifecycle - Job flow, error handling
- Architecture Overview - System overview