API-based Integration Patterns
This document covers the core patterns and code organization for API-based integrations that connect to external services via HTTP, OAuth, or SOAP.
Class Structure Pattern
Section titled “Class Structure Pattern”Each API-based integration follows this structure:
app/Integrations/[Vendor]/├── Api.php # API client wrapper (optional, for complex APIs)├── Connector.php # Transport: authentication + HTTP calls├── Integrator.php # Core sync logic, implements EntityProvider├── Importer.php # Household import logic├── Sync.php # Sync handler (legacy pattern)├── Config.php # Integration configuration├── Models/ # Vendor-specific data models│ ├── Account.php│ ├── Holding.php│ ├── Transaction.php│ └── ...├── Exceptions/ # Vendor-specific exceptions└── OAuth2/ # OAuth configuration (if OAuth-based) └── Provider.phpConnector Design
Section titled “Connector Design”Connector Hierarchy
Section titled “Connector Hierarchy”Connector (abstract base) └── ConnectorWithIntegration (abstract, has Integration model) ├── HttpConnectorWithIntegration ├── OauthConnectorWithIntegration └── SoapConnectorWithIntegrationLocation: app/Integrations/Support/LegacyApiBased/Connectors/
Base Connector
Section titled “Base Connector”abstract class Connector{ // Logging utilities for debugging protected static function logToFile(?string $reference, string $postfix, array $data): void;}ConnectorWithIntegration
Section titled “ConnectorWithIntegration”Base class for integrations that have an Integration model instance.
abstract class ConnectorWithIntegration extends Connector{ public function __construct(protected Integration $integration) {}
// Make API call to vendor abstract public function call(string $relative_url): array;
// Verify stored credentials are still valid public function verifyIntegrationCredentials(): bool;
// Prevent duplicate integrations per advisor protected function checkIfMoreThanOneIntegration(): void;}HttpConnectorWithIntegration
Section titled “HttpConnectorWithIntegration”For REST APIs with API Key or Token authentication.
class HttpConnectorWithIntegration extends ConnectorWithIntegration{ use UsesHttpTransport;
public function call(string $relative_url): array { // Build request with auth headers // Make HTTP call via Guzzle // Handle response/errors }}Used by: Blueleaf, Commonwealth, Wealthbox, Redtail, etc.
OauthConnectorWithIntegration
Section titled “OauthConnectorWithIntegration”For OAuth 2.0 flows with token refresh.
class OauthConnectorWithIntegration extends ConnectorWithIntegration{ use UsesHttpTransport;
protected ThreadSafeOAuthTokenManager $token_manager;
public function call(string $relative_url): array { // Check token expiry // Refresh if needed (thread-safe) // Make authenticated request }
public function getAccessToken(): AccessToken; public function refreshAccessToken(): AccessToken;}Used by: Schwab API, Addepar, Orion, Capitect, Advyzon, etc.
Supporting Classes:
ThreadSafeOAuthTokenManager- Handles concurrent token refresh safelyJsonAuthOptionProvider- Custom OAuth option providerOauthSamlProvider- SAML-based OAuth providerOauthBasicAuthWithoutRefreshToken- Basic auth variant
SoapConnectorWithIntegration
Section titled “SoapConnectorWithIntegration”For legacy SOAP/XML-RPC services.
class SoapConnectorWithIntegration extends ConnectorWithIntegration{ use UsesSoapTransport;
protected SoapClient $client;
public function call(string $method): array { // Build SOAP envelope // Make SOAP call // Parse XML response }}Used by: Tamarac
Integrator Design
Section titled “Integrator Design”Base Integrator (from integrations-core)
Section titled “Base Integrator (from integrations-core)”abstract class Integrator{ public function __construct(protected Integration $integration) {}
public function getIntegration(): Integration { return $this->integration; }
// Sync all linked accounts, remove disappeared, restore re-appeared abstract public function syncAll(): void;
// Sync single mapping, return imported data abstract public static function sync(IntegrationMapping $mapping): array|null;
// Get vendor type abstract protected static function getVendor(): IntegrationType;
// Debug logging protected static function logToFile(?string $reference, string $postfix, array $data): void;}EntityProvider Interface
Section titled “EntityProvider Interface”Integrators that can list available entities implement this interface:
interface EntityProvider{ /** * List entities with optional search and cursor-based pagination. * * @param array<string,mixed> $conditions Search conditions * @param string|null $cursor Pagination cursor, null = first page * @return CursorPage<TEntity> Page of entities with next cursor */ public function listEntities(array $conditions, ?string $cursor = null): CursorPage;}CursorPage Result
Section titled “CursorPage Result”final readonly class CursorPage{ public function __construct( public array $data, // list<TEntity> public ?string $next_cursor = null ) {}}Typical Integrator Implementation
Section titled “Typical Integrator Implementation”class Integrator extends \RightCapital\IntegrationsCore\Integrators\Integrator implements EntityProvider{ protected Connector $connector;
public function __construct(Integration $integration) { parent::__construct($integration); $this->connector = new Connector($integration); }
// List available contacts from Wealthbox public function listEntities(array $conditions, ?string $cursor = null): CursorPage { $response = $this->connector->call('/contacts', [ 'search' => $conditions['search'] ?? null, 'page' => $cursor, ]);
return new CursorPage( data: array_map(fn($c) => [ 'reference' => $c['id'], 'name' => $c['name'], 'email' => $c['email'], ], $response['contacts']), next_cursor: $response['next_page'] ?? null ); }
public function syncAll(): void { foreach ($this->integration->getRootIntegrationMappings() as $mapping) { static::sync($mapping); } }
public static function sync(IntegrationMapping $mapping): ?array { $connector = new Connector($mapping->integration); $contact = $connector->call("/contacts/{$mapping->reference}");
// Map vendor data to internal models // Update Account, Insurance, etc. // Update mapping->last_completed_at
return ['households' => [...], 'accounts' => [...], 'persons' => [...]]; }
protected static function getVendor(): IntegrationType { return IntegrationType::WEALTHBOX; }}Authentication Patterns
Section titled “Authentication Patterns”OAuth 2.0 (Authorization Code Flow)
Section titled “OAuth 2.0 (Authorization Code Flow)”Used by: Schwab API, Addepar, Orion, Capitect, Advyzon, Allianz API
// OAuth2 Provider configurationclass 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']; }}Flow:
- User redirected to vendor authorization URL
- User grants access, redirected back with code
- Exchange code for access_token + refresh_token
- Store tokens in
integration.credentials - Use access_token for API calls
- Refresh when expired (ThreadSafeOAuthTokenManager)
JWT Token (Yodlee)
Section titled “JWT Token (Yodlee)”// Connector generates JWT for each requestclass Connector extends ConnectorWithIntegration{ private function generateJwt(): string { $payload = [ 'iss' => $this->integration->credentials['issuer'], 'sub' => $this->integration->credentials['user_id'], 'iat' => time(), 'exp' => time() + 3600, ];
return JWT::encode($payload, $this->getPrivateKey(), 'RS256'); }
public function call(string $url): array { return Http::withToken($this->generateJwt()) ->get($this->baseUrl . $url) ->json(); }}API Key / Token
Section titled “API Key / Token”Used by: Wealthbox, Redtail, Betterment
class Connector extends HttpConnectorWithIntegration{ protected function getAuthHeaders(): array { return [ 'Authorization' => 'Bearer ' . $this->integration->credentials['api_key'], // or 'X-Api-Key' => $this->integration->credentials['api_key'], ]; }}Basic Auth
Section titled “Basic Auth”class Connector extends HttpConnectorWithIntegration{ protected function getAuthHeaders(): array { $credentials = base64_encode( $this->integration->credentials['username'] . ':' . $this->integration->credentials['password'] );
return ['Authorization' => 'Basic ' . $credentials]; }}Model Layer
Section titled “Model Layer”Vendor Models
Section titled “Vendor Models”Each integration defines vendor-specific models that mirror the API response structure:
class Contact{ public function __construct( public string $id, public string $name, public ?string $email, public array $accounts, ) {}
public static function fromApiResponse(array $data): self { return new self( id: $data['id'], name: $data['first_name'] . ' ' . $data['last_name'], email: $data['email'] ?? null, accounts: $data['accounts'] ?? [], ); }}Core Models (from integrations-core)
Section titled “Core Models (from integrations-core)”// packages/libs/integrations-core/src/Models/Account // Base account model with traitsInsurance // Base insurance model with traitsHolding // Position/holding model
// Account type traitsBankAccountTraitCardAccountTraitInvestmentAccountTraitLoanAccountTrait
// Insurance type traitsLifeInsuranceTraitDisabilityInsuranceTraitAutoInsuranceTraitLongTermCareInsuranceTraitException Handling
Section titled “Exception Handling”Exception Hierarchy (from integrations-core)
Section titled “Exception Hierarchy (from integrations-core)”Exception (abstract base)├── ExternalServiceException (vendor API errors)│ ├── ConnectException // Connection failed│ ├── TimeoutException // Request timeout│ ├── UnauthorizedException // Auth failed (401)│ ├── ForbiddenException // Access denied (403)│ ├── NotFoundException // Entity not found (404)│ ├── TooManyRequestException // Rate limited (429)│ ├── ServiceUnavailableException // Vendor down (503)│ └── UnrecognizedException // Unknown error├── FileDeliveryException // File send failed├── FileNotFoundException // File not found├── FileOutdatedException // File too old└── UnexpectedValueException // Invalid dataAffectedLevel Enum
Section titled “AffectedLevel Enum”Indicates whether the error affects the entire integration or just one mapping:
enum AffectedLevel: string{ case Integration = 'integration'; // Whole integration broken case IntegrationMapping = 'mapping'; // Single mapping failed}Usage in Integrators
Section titled “Usage in Integrators”public static function sync(IntegrationMapping $mapping): ?array{ try { $connector = new Connector($mapping->integration); $data = $connector->call("/accounts/{$mapping->reference}"); // ... process data } catch (UnauthorizedException $e) { // Credentials expired - mark integration as failed throw new ExternalServiceException( integration: $mapping->integration, affected_level: AffectedLevel::Integration, message: 'Credentials expired, please reconnect', previous: $e ); } catch (NotFoundException $e) { // Account deleted on vendor side - soft delete mapping $mapping->deleteByUserManually(); return null; }}Related Documentation
Section titled “Related Documentation”- Sync Lifecycle - Job flow, triggers, error recovery
- Vendor Specifics - Yodlee, Schwab, Orion details
- Architecture Overview - System overview