SnapTrade Integration#
Overview#
SnapTrade is Sure's brokerage account provider, enabling users to connect 25+ major brokerages (Fidelity, Vanguard, Schwab, Robinhood, etc.) to import investment holdings, cost basis, and trade history. It is distinct from bank/transaction providers (SimpleFIN, Lunchflow) — SnapTrade covers investment accounts only.
SnapTrade is registered as a US/CA investment provider with beta maturity in provider/metadata.rb . The integration supports Investment and Crypto account types .
Key Files#
| Layer | File | Purpose |
|---|---|---|
| API client | app/models/provider/snaptrade.rb | Wraps the SnapTrade Ruby SDK; all API calls go through here |
| Provider adapter | app/models/provider/snaptrade_adapter.rb | Registers SnapTrade in the provider factory; routing/metadata |
| Domain model | app/models/snaptrade_item.rb | Stores credentials, OAuth tokens, sync state per family |
| Auth/connection | app/models/snaptrade_item/provided.rb | User registration, OAuth device flow, connection portal URL |
| Sync orchestrator | app/models/snaptrade_item/syncer.rb | Phases: import → process holdings/activities → calc balances |
| Importer | app/models/snaptrade_item/importer.rb | Fetches accounts, holdings, activities, balances from API |
| Brokerage account | app/models/snaptrade_account.rb | Individual brokerage account; links to Sure Account via AccountProvider |
| Holdings processor | app/models/snaptrade_account/holdings_processor.rb | Creates Security records; surfaces multi-currency cash as synthetic holdings |
| Activities processor | app/models/snaptrade_account/activities_processor.rb | Maps SnapTrade activity types → Sure labels (Buy, Sell, Dividend, etc.) |
| Controller | app/controllers/snaptrade_items_controller.rb | OAuth flows, account setup, linking, connection management |
| Activities job | app/jobs/snaptrade_activities_fetch_job.rb | Async activities fetch with retry; handles brokerage sync delays |
| Cleanup job | app/jobs/snaptrade_connection_cleanup_job.rb | Deletes connections from SnapTrade API when accounts are destroyed |
| i18n strings | config/locales/views/snaptrade_items/en.yml | All UI copy |
Authentication#
The integration supports two auth modes, selectable per setup:
1. Legacy API Credentials#
Users supply their own Client ID and Consumer Key from dashboard.snaptrade.com. These are stored encrypted on SnaptradeItem using ActiveRecord Encryption — client_id with deterministic encryption (queryable), consumer_key and secrets with non-deterministic encryption .
2. OAuth Device Flow (preferred)#
Uses SnapTrade's device authorization grant (urn:ietf:params:oauth:grant-type:device_code). The flow:
- Client calls
start_oauth_device_flow→Provider::Snaptrade#start_device_authorizationhits the device authorization endpoint - User is shown a device code and directed to the SnapTrade URL to authorize
- Client polls
complete_oauth_device_flow!→Provider::Snaptrade#poll_device_token - Access/refresh tokens are stored encrypted on
SnaptradeItem
The device flow requires SNAPTRADE_OAUTH_CLIENT_ID in the environment — set at the deployment level, not per-user . If not set, the UI displays an error directing admins to add it to .env.local .
The OAuth discovery URL is https://api.snaptrade.com/.well-known/oauth-authorization-server .
User Registration & Connection Flow#
Each Sure family gets a corresponding SnapTrade user (identified by snaptrade_user_id and snaptrade_user_secret), registered via register_user. This is separate from OAuth auth — it gives the user a "slot" on SnapTrade's platform.
Once registered, connecting a brokerage uses connection_portal_url, which calls Provider::Snaptrade#get_connection_url. The optional broker: parameter enables pass-through broker selection — when specified, SnapTrade opens its portal pre-filtered to that broker rather than showing the full broker list. This is how Sure passes broker choice through to SnapTrade's hosted UI.
After the OAuth redirect back, the callback flow (route: GET /snaptrade_items/callback) prompts the user to select which discovered accounts to link to Sure accounts .
The free tier allows 20 brokerage connections . Orphaned user registrations (e.g., from previous family setups) can consume connection slots and are surfaced in the UI for deletion .
Sync Pipeline#
After accounts are linked, syncing follows this pipeline :
- Import (
SnaptradeItem::Importer) — fetches accounts, holdings, positions, and balances from SnapTrade API - Process holdings (
SnaptradeAccount::HoldingsProcessor) — createsSecurityrecords, handles multi-currency cash as synthetic holdings - Process activities (
SnaptradeAccount::ActivitiesProcessor) — maps SnapTrade activity types to Sure labels; activities are often fetched asynchronously viaSnaptradeActivitiesFetchJobdue to brokerage sync delays of 30–60+ seconds for fresh connections - Calculate balances — scheduled as a separate step after data is settled
Up to 3 years of transaction history is available . The sync_start_date field on SnaptradeAccount lets users limit import to a specific date.
Routes#
SnapTrade-specific routes are nested under snaptrade_items :
- Collection:
callback,oauth_connect,start_oauth_connect,preload_accounts,select_accounts,link_accounts,select_existing_account,link_existing_account - Member:
sync,connect,setup_accounts,complete_account_setup,connections,start_oauth_device_flow,complete_oauth_device_flow,delete_connection,delete_orphaned_user
Error Handling & Reliability#
Provider::Snaptrade wraps all API calls with retry logic: up to 3 retries with exponential backoff + 25% jitter, capped at 30 seconds, for transient network errors . HTTP 401/403 raises AuthenticationError; 429 raises a rate-limit ApiError; 5xx raises a server ApiError .