# How Securo Handles Transaction Management via Its API: A Complete Technical Guide

> Discover how Securo handles transaction management via its API. Explore CRUD operations, bulk actions, and more with this technical guide to the Securo API.

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

---

**Securo implements transaction management through a FastAPI-based router in [`backend/app/api/transactions.py`](https://github.com/securo-finance/securo/blob/main/backend/app/api/transactions.py) that exposes CRUD operations, transfer pairing, bulk actions, and import previews, backed by SQLAlchemy models with JWT authentication and workspace scoping.**

Every financial operation in Securo flows through a structured API layer designed for reliability and scalability. The `securo-finance/securo` repository demonstrates how modern Python backends can handle complex transaction logic—including automatic transfer pairing and cascade operations—while maintaining clean separation between routing, business logic, and data persistence.

## Transaction API Architecture

Securo's transaction management follows a three-layer architecture typical of production FastAPI applications.

### Routing Layer

The **FastAPI router** in [`backend/app/api/transactions.py`](https://github.com/securo-finance/securo/blob/main/backend/app/api/transactions.py) registers all transaction endpoints. This file handles:

- Request validation through Pydantic schemas
- JWT authentication and workspace extraction from request context
- Dependency injection for database sessions
- Route dispatch to appropriate handler functions

### Service Layer

Business logic resides in [`backend/app/services/transaction_service.py`](https://github.com/securo-finance/securo/blob/main/backend/app/services/transaction_service.py) (when present) or directly in the router. Key responsibilities include:

- **Transfer pairing**: Creating linked debit/credit transaction pairs
- **Cascade operations**: Propagating updates and deletions to paired transactions
- **Bulk actions**: Efficient batch updates for transaction groups
- **Import validation**: Parsing files without database mutations

### Data Model Layer

The `Transaction` model in [`backend/app/models/transaction.py`](https://github.com/securo-finance/securo/blob/main/backend/app/models/transaction.py) defines the core schema:

```python

# Key fields in the Transaction SQLAlchemy model

- id: UUID primary key
- date: Date of transaction
- amount: Decimal amount
- type: Enum ("debit" | "credit")
- account_id: Foreign key to accounts table
- category_id: Foreign key to categories table
- transfer_pair_id: Nullable UUID linking paired transfers
- workspace_id: Multi-tenant isolation field

```

## Complete Transaction Endpoint Reference

| HTTP Method | Endpoint | Purpose |
|-------------|----------|---------|
| `GET` | `/api/transactions` | List with filtering, sorting, pagination |
| `POST` | `/api/transactions` | Create single transaction |
| `GET` | `/api/transactions/{id}` | Retrieve specific transaction |
| `PATCH` | `/api/transactions/{id}` | Partial update with cascade to pairs |
| `DELETE` | `/api/transactions/{id}` | Delete with cascade for transfers |
| `POST` | `/api/transactions/transfer` | Create paired transfer (debit + credit) |
| `POST` | `/api/transactions/link-transfer` | Link two existing transactions |
| `GET` | `/api/transactions/{id}/transfer-candidates` | Suggest matching transactions |
| `POST` | `/api/transactions/{id}/create-counterpart` | Generate missing transfer pair |
| `GET` | `/api/transactions/import/preview` | Parse import file (read-only) |
| `POST` | `/api/transactions/bulk-add-to-group` | Batch group assignment |

All endpoints enforce **workspace scoping**—queries automatically filter to the authenticated user's workspace, preventing cross-tenant data access.

## Working with the Transaction API

### Listing Transactions with Filters

The `GET /api/transactions` endpoint supports rich filtering for building responsive financial dashboards.

```python
import requests

headers = {"Authorization": "Bearer <TOKEN>"}
params = {
    "type": "credit",
    "q": "coffee",              # Text search on description

    "min_amount": 10,
    "max_amount": 500,
    "date_from": "2024-01-01",
    "date_to": "2024-12-31",
    "exclude_transfers": "true",  # Hide internal transfers

    "sort_by": "-date",          # Descending date

    "page": 1,
    "page_size": 50
}

resp = requests.get(
    "<BASE_URL>/api/transactions",
    headers=headers,
    params=params
)
transactions = resp.json()

```

The `exclude_transfers` parameter is particularly useful for financial reporting—transfer transactions represent money movement between owned accounts and typically shouldn't count as income or expenses.

### Creating Standard Transactions

Single transactions require minimal payload while supporting full categorization.

```python
payload = {
    "date": "2024-08-15",
    "amount": 84.50,
    "type": "debit",
    "account_id": "acc-checking-001",
    "category_id": "cat-dining-001",
    "description": "Team lunch - Q3 planning"
}

resp = requests.post(
    "<BASE_URL>/api/transactions",
    json=payload,
    headers={"Authorization": "Bearer <TOKEN>"}
)

new_tx = resp.json()
print(f"Created transaction: {new_tx['id']}")

```

### Handling Transfer Transactions

Transfers require special handling because they involve two accounts. Securo provides two approaches:

**Approach 1: Create both sides simultaneously**

```python
payload = {
    "debit": {
        "date": "2024-08-15",
        "amount": 1000.00,
        "account_id": "acc-checking-001",
        "description": "Monthly savings transfer"
    },
    "credit": {
        "date": "2024-08-15",
        "amount": 1000.00,
        "account_id": "acc-savings-001",
        "description": "Monthly savings transfer"
    }
}

resp = requests.post(
    "<BASE_URL>/api/transactions/transfer",
    json=payload,
    headers={"Authorization": "Bearer <TOKEN>"}
)

result = resp.json()

# Returns both transaction IDs with matching transfer_pair_id

print(f"Debit: {result['debit']['id']}")
print(f"Credit: {result['credit']['id']}")
print(f"Pair ID: {result['transfer_pair_id']}")

```

**Approach 2: Link existing transactions**

When transactions are imported separately (e.g., from different bank feeds), use the link endpoint:

```python
payload = {
    "transaction_ids": ["tx-debit-uuid", "tx-credit-uuid"]
}

resp = requests.post(
    "<BASE_URL>/api/transactions/link-transfer",
    json=payload,
    headers={"Authorization": "Bearer <TOKEN>"}
)

```

The API validates that neither transaction already has a `transfer_pair_id` before creating the link.

### Bulk Operations for Performance

For batch classification workflows, the bulk endpoint reduces API calls:

```python
payload = {
    "transaction_ids": ["tx-001", "tx-002", "tx-003", "tx-004"],
    "group_id": "group-business-travel"
}

resp = requests.post(
    "<BASE_URL>/api/transactions/bulk-add-to-group",
    json=payload,
    headers={"Authorization": "Bearer <TOKEN>"}
)

```

This operation is atomic—either all transactions are updated or none are.

### Safe Import with Preview

The import preview endpoint allows validation without database changes:

```python
files = {"file": ("statement.csv", open("august_2024.csv", "rb"), "text/csv")}

resp = requests.post(
    "<BASE_URL>/api/transactions/import/preview",
    files=files,
    headers={"Authorization": "Bearer <TOKEN>"}
)

preview = resp.json()

# preview contains parsed rows with validation warnings

# No database writes occur at this stage

```

## Cascade Behavior and Data Integrity

Securo's transaction management maintains consistency through database-level and application-level cascades:

| Operation | Cascade Behavior | Implementation Location |
|-----------|-----------------|------------------------|
| Update transfer transaction | Propagates description/date changes to pair | [`transaction_service.py`](https://github.com/securo-finance/securo/blob/main/transaction_service.py) or router |
| Delete transfer transaction | Deletes counterpart transaction | SQLAlchemy relationship + service logic |
| Delete account | Handles associated transactions per policy | Account service layer |

When deleting a transfer, both sides are removed automatically:

```python

# Deleting tx-abc-123 which has transfer_pair_id set

resp = requests.delete(
    "<BASE_URL>/api/transactions/tx-abc-123",
    headers={"Authorization": "Bearer <TOKEN>"}
)

# Returns 204; counterpart transaction also deleted

```

## Source Code Reference

Understanding the implementation requires examining these key files in `securo-finance/securo`:

- **[`backend/app/api/transactions.py`](https://github.com/securo-finance/securo/blob/main/backend/app/api/transactions.py)** — FastAPI router with all endpoint definitions
- **[`backend/app/models/transaction.py`](https://github.com/securo-finance/securo/blob/main/backend/app/models/transaction.py)** — SQLAlchemy model with transfer pairing logic
- **[`backend/app/services/transaction_service.py`](https://github.com/securo-finance/securo/blob/main/backend/app/services/transaction_service.py)** — Business logic for complex operations
- **`backend/alembic/versions/`** — Database migrations for transaction schema evolution

The test suite in [`backend/tests/test_transactions_api_coverage.py`](https://github.com/securo-finance/securo/blob/main/backend/tests/test_transactions_api_coverage.py) provides additional documentation through executable examples of expected behavior.

## Summary

- **Securo's transaction API** is implemented as a FastAPI router in [`backend/app/api/transactions.py`](https://github.com/securo-finance/securo/blob/main/backend/app/api/transactions.py) with comprehensive CRUD, transfer management, and bulk operations
- **Transfer pairing** uses `transfer_pair_id` to link related transactions, with automatic cascade updates and deletions
- **Workspace scoping** enforces multi-tenant isolation on every database query
- **Import preview** provides safe, read-only validation before data persistence
- **Bulk endpoints** optimize performance for common classification workflows

## Frequently Asked Questions

### How does Securo prevent duplicate transfer transactions?

The API validates `transfer_pair_id` before linking. When using `POST /api/transactions/link-transfer`, both transactions must have null `transfer_pair_id` values. The database likely enforces unique constraints on pairing combinations, and the service layer checks for existing pairs before creating new links.

### Can I update only one side of a transfer transaction?

Partial updates via `PATCH /api/transactions/{id}` cascade to the paired transaction for certain fields (description, date). Amount and account fields typically remain independent since they represent the actual financial movement. The specific cascade rules are implemented in the update handler within [`backend/app/api/transactions.py`](https://github.com/securo-finance/securo/blob/main/backend/app/api/transactions.py).

### What happens to transactions when I delete an account?

This depends on the account deletion policy configured in [`backend/app/services/account_service.py`](https://github.com/securo-finance/securo/blob/main/backend/app/services/account_service.py). Common approaches include: blocking deletion with existing transactions, soft-deleting the account while preserving transaction history, or cascading deletion with transfer reclassification. The transaction records maintain referential integrity through foreign key constraints.

### Is the import preview endpoint completely safe?

Yes. The `GET /api/transactions/import/preview` endpoint parses uploaded files and returns structured data without initiating any database write operations. It operates as a pure function of the input file, making it safe to call repeatedly during import workflow UIs.