# How Securo Manages Bank Connections via API: A Complete Technical Guide

> Learn how Securo manages bank connections via API using OAuth 2.0 and FastAPI for secure token generation and data synchronization with open finance providers. Get the technical guide.

- Repository: [securo-finance/securo](https://github.com/securo-finance/securo)
- Tags: deep-dive
- Published: 2026-08-28

---

**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](https://github.com/securo-finance/securo/blob/main/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:

```python
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:

```python
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](https://github.com/securo-finance/securo/blob/main/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):

1. Fetches provider tokens via `provider.handle_oauth_callback(code)`
2. Creates or updates a `BankConnection` record with normalized metadata
3. Associates the connection with the original workspace and user
4. Returns a complete `BankConnectionRead` object

```python
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](https://github.com/securo-finance/securo/blob/main/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

```python

# 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`:

```python
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 `BankConnection` based on data retention policy
- Cascades to associated `Account` rows

## Data Synchronization and Asset Handling

### Manual Sync Trigger

Users or schedules can force fresh data pulls:

```python
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:

1. Forces the provider to fetch latest bank data
2. Creates or updates `Account` rows for each linked bank account
3. Pulls transactions via `provider.get_transactions`
4. Conditionally executes `_sync_holdings` if `settings.sync_assets` is 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:

```python
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 `credentials` column
- **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](https://github.com/securo-finance/securo/blob/main/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/providers` enables 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 `BankConnection` records 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.