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:
backend/app/api/transactions.py— FastAPI router with all endpoint definitionsbackend/app/models/transaction.py— SQLAlchemy model with transfer pairing logicbackend/app/services/transaction_service.py— Business logic for complex operationsbackend/alembic/versions/— Database migrations for transaction schema evolution
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.pywith comprehensive CRUD, transfer management, and bulk operations - Transfer pairing uses
transfer_pair_idto 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →