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

Securo implements transaction management through a FastAPI-based router in 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 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 (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 defines the core schema:


# 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.

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.

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

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:

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:

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:

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


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

The test suite in 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 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.

What happens to transactions when I delete an account?

This depends on the account deletion policy configured in 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.

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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →