How Securo Manages Bank Connections via API: A Complete Technical Guide
Securo manages bank connections through OAuth 2.0-based FastAPI endpoints that handle token generation, secure state management, and automated data synchronization across multiple open-finance providers.
Securo's banking API abstracts provider-specific complexity behind a unified interface, allowing developers to connect user accounts from services like Pluggy and SimpleFIN without handling credentials directly. The system implements a complete connection lifecycle—from initial discovery through re-authentication—using encrypted storage, CSRF-protected OAuth flows, and idempotent sync operations.
Provider Discovery and Connect Token Generation
Before initiating a connection, clients discover available providers and request short-lived connect tokens for frontend widgets.
Listing Supported Providers
The GET /api/connections/providers endpoint returns all configured open-finance integrations at backend/app/api/connections.py (lines 36-40). This enables dynamic UI rendering based on enabled providers.
Connect Token Creation
To start the linking flow, the frontend requests a connect token:
import requests
BASE = "https://api.securo.example.com"
HEADERS = {"Authorization": "Bearer <user-jwt>"}
# Create a connect token for the widget
payload = {"provider": "pluggy"}
resp = requests.post(f"{BASE}/api/connections/connect-token", json=payload, headers=HEADERS)
token = resp.json()["access_token"]
The POST /api/connections/connect-token endpoint (lines 42-50) delegates to connection_service.create_connect_token, which invokes the provider's native token method—such as Pluggy's link token—and returns {access_token: …} for widget initialization.
OAuth Flow Implementation
Securo implements a state-protected OAuth 2.0 flow with explicit separation between URL generation and callback handling.
Generating OAuth URLs
POST /api/connections/oauth/url (lines 68-78) prepares the redirect:
payload = {"provider": "pluggy", "flow_params": {"sync_assets": True}}
resp = requests.post(f"{BASE}/api/connections/oauth/url", json=payload, headers=HEADERS)
oauth_url = resp.json()["url"]
The endpoint stores a state blob via the OAuth state service (backend/app/services/oauth_state.py) containing user_id, workspace_id, provider, and optional flow_params. This prevents CSRF attacks by binding each OAuth URL to its originating request context.
Handling OAuth Callbacks
After user authorization, providers redirect to POST /api/connections/oauth/callback (lines 99-131). The callback validates the stored state against the incoming request, then executes handle_oauth_callback in the connection service (lines 21-84):
- Fetches provider tokens via
provider.handle_oauth_callback(code) - Creates or updates a
BankConnectionrecord with normalized metadata - Associates the connection with the original workspace and user
- Returns a complete
BankConnectionReadobject
payload = {"code": "<auth-code>", "provider": "pluggy", "state": "<state-id>"}
resp = requests.post(f"{BASE}/api/connections/oauth/callback", json=payload, headers=HEADERS)
connection = resp.json() # BankConnectionRead object
For reconnections, Securo reuses the existing BankConnection ID rather than creating duplicates, preserving historical transaction data.
Connection Data Model and Storage
The BankConnection model (backend/app/models/bank_connection.py) persists all connection state:
| Field | Purpose |
|---|---|
provider |
Provider identifier ("pluggy", "simplefin", etc.) |
external_id |
Provider-assigned stable connection identifier |
institution_name / logo_url |
Human-readable display metadata |
credentials |
Encrypted JSON blob of provider-specific tokens |
settings |
Per-connection toggles (sync_assets, etc.) |
The model maintains relationships to Account, AssetGroup, and Institution, forming the hierarchy: Workspace → BankConnection → Account → Transactions/Holdings.
Connection CRUD Operations
Beyond creation, the API supports full lifecycle management:
Listing and Retrieving Connections
# List all workspace connections
resp = requests.get(f"{BASE}/api/connections", headers=HEADERS)
connections = resp.json()
connection_service.get_connections filters by the authenticated user's workspace.
Updating Connection Settings
Users can modify per-connection behavior via PATCH /api/connections/{connection_id}/settings:
payload = {"sync_assets": False}
requests.patch(f"{BASE}/api/connections/{conn_id}/settings", json=payload, headers=HEADERS)
The connection_service.update_connection_settings method restricts updates to user-editable fields only, protecting internal state.
Deleting Connections
DELETE /api/connections/{connection_id} invokes connection_service.delete_connection, which:
- Revokes provider tokens where supported
- Soft-deletes or hard-deletes the
BankConnectionbased on data retention policy - Cascades to associated
Accountrows
Data Synchronization and Asset Handling
Manual Sync Trigger
Users or schedules can force fresh data pulls:
requests.post(f"{BASE}/api/connections/{conn_id}/sync", headers=HEADERS)
The POST /api/connections/{connection_id}/sync endpoint (lines 63-82) calls sync_connection with trigger_provider_refresh=True, which:
- Forces the provider to fetch latest bank data
- Creates or updates
Accountrows for each linked bank account - Pulls transactions via
provider.get_transactions - Conditionally executes
_sync_holdingsifsettings.sync_assetsis enabled
Investment Holdings Sync
When sync_assets is true, the sync process retrieves investment positions through provider-specific implementations, storing them in AssetGroup and related models.
Re-authentication and Connection Recovery
Expired credentials or provider-mandated re-consent use dedicated endpoints that preserve connection history:
Reconnect Token
POST /api/connections/{connection_id}/reconnect-token (lines 39-56) generates a widget-compatible token for the existing connection, avoiding duplicate records.
Re-auth URL
For OAuth-based re-authorization:
resp = requests.post(f"{BASE}/api/connections/{conn_id}/oauth/reauth-url", headers=HEADERS)
reauth_url = resp.json()["url"]
This reuses the stored connection ID while refreshing the OAuth state, ensuring the re-linked account maps to the same BankConnection record.
Security Architecture
Securo's bank connection API implements multiple security layers:
- Encrypted credentials at rest: Provider tokens stored as encrypted JSON in the
credentialscolumn - CSRF protection: OAuth state service validates state parameters match the original request context
- Scoped tokens: Connect tokens are short-lived and single-use for widget initialization
- Workspace isolation: All queries filter by the authenticated user's workspace membership
The oauth_state service (backend/app/services/oauth_state.py) provides the cryptographic binding between OAuth initiation and completion, preventing session fixation attacks.
Summary
- Provider discovery via
GET /api/connections/providersenables dynamic UI configuration - Connect tokens bridge Securo's API to provider-specific widget SDKs
- OAuth state management prevents CSRF while allowing flexible flow parameters
- Idempotent callbacks create or update
BankConnectionrecords without duplicates - Encrypted credential storage protects provider tokens with workspace-scoped access
- Manual and scheduled sync pulls transactions and optionally investment holdings
- Re-auth endpoints preserve connection history across credential refreshes
Frequently Asked Questions
How does Securo prevent duplicate bank connections?
The OAuth callback handler in connection_service.handle_oauth_callback checks for existing BankConnection records matching the provider and external_id combination. For re-authentication flows, the re-auth and reconnect endpoints explicitly pass the existing connection_id, ensuring the provider link updates credential storage without creating new records.
What data is stored in the BankConnection credentials field?
The credentials column contains a provider-specific encrypted JSON blob—for Pluggy, this includes access tokens and refresh tokens; for SimpleFIN, API keys or OAuth tokens. The exact schema varies by provider implementation, but all values are encrypted at rest using the application's configured encryption key.
Can I disable investment holdings sync for specific connections?
Yes. The settings.sync_assets boolean flag controls whether the _sync_holdings method executes during sync. Update this via PATCH /api/connections/{connection_id}/settings with {"sync_assets": false}. This is useful for connections where only transactional accounts matter, reducing sync time and storage consumption.
How long are connect tokens valid?
Connect tokens generated via POST /api/connections/connect-token are short-lived by design—typically 15-30 minutes depending on provider requirements. They are single-use tokens intended solely for initializing the provider's frontend widget. Persistent authentication uses the encrypted credentials stored in the BankConnection model.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →