Transfers
Transfers represent money movements between accounts within Crane Ledger. They automatically create balanced double-entry transactions and are used for cash management, account reconciliations, and internal fund allocations.
The Transfer Object
/organizations/:organization_id/transfersA transfer represents a money movement between two accounts.
{
"id": "trf_xxxxxxxxxxxxxxxx",
"object": "transfer",
"organization_id": "org_xxxxxxxxxxxxxxxx",
"from_account_id": "act_xxxxxxxxxxxxxxxx",
"to_account_id": "act_yyyyyyyyyyyyyyyy",
"expense_transaction_id": "txn_xxxxxxxxxxxxxxxx",
"income_transaction_id": "txn_yyyyyyyyyyyyyyyy",
"amount": "5000.00",
"currency_id": "CUR_USD",
"currency_rate": "1.0000",
"transferred_at": "2024-01-15",
"description": "Transfer to operating account",
"reference": "TRF-2024-001",
"created_by": "usr_xxxxxxxxxxxxxxxx",
"created_at": "2024-01-15T10:30:00Z",
"updated_at": "2024-01-15T10:30:00Z",
"metadata": {
"purpose": "cash_management",
"approved_by": "finance_manager"
}
}
Attributes
| Attribute | Type | Description |
|---|---|---|
id | string | Unique identifier with trf_ prefix |
from_account_id | string | Source account ID |
to_account_id | string | Destination account ID |
amount | string | Transfer amount |
currency_id | string | Transfer currency |
transferred_at | date | Transfer date |
description | string | Transfer description |
reference | string | Reference number |
expense_transaction_id | string | Expense transaction created |
income_transaction_id | string | Income transaction created |
Create Transfer
/organizations/:organization_id/transfersCreates a transfer between two accounts, automatically generating the corresponding double-entry transactions.
Path Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
organization_id | string | Yes | The ID of the organization |
Request Body
| Parameter | Type | Required | Description |
|---|---|---|---|
from_account_id | string | Yes | Source account ID |
to_account_id | string | Yes | Destination account ID |
amount | number | Yes | Transfer amount |
transferred_at | date | Yes | Transfer date (YYYY-MM-DD) |
description | string | No | Transfer description |
reference | string | No | Reference number |
Response
{
"request_id": "req_xxxxxxxxxxxxxxxx",
"success": true,
"data": {
"status": "queued",
"estimated_completion": "2024-01-15T10:30:05Z",
"transfer_preview": {
"from_account": "Operating Account",
"to_account": "Savings Account",
"amount": "50000.00",
"expense_transaction_id": "txn_xxxxxxxxxxxxxxxx",
"income_transaction_id": "txn_yyyyyyyyyyyyyyyy"
}
},
"error": null,
"duration_ms": 0,
"credit_cost": 1
}
Asynchronous Processing
Transfer creation is processed asynchronously. The response includes a request_id that can be used to check the status of the operation. The transfer will be available for use once processing is complete.
List Transfers
/organizations/:organization_id/transfersReturns a list of transfers for the organization with optional filtering.
Path Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
organization_id | string | Yes | The organization ID |
Query Parameters
| Parameter | Type | Description |
|---|---|---|
from_account_id | string | Filter by source account |
to_account_id | string | Filter by destination account |
limit | integer | Number of results (default: 20, max: 100) |
offset | integer | Pagination offset |
Response
{
"object": "list",
"data": [
{
"id": "trn_xxxxxxxxxxxxxxxx",
"amount": "5000.00",
"from_account_name": "Operating Checking",
"to_account_name": "Savings Account",
"transfer_date": "2024-01-15",
"description": "Monthly savings transfer",
"created_at": "2024-01-15T10:30:00Z"
}
],
"has_more": false,
"total_count": 1
}
Get Transfer
/organizations/:organization_id/transfers/:idRetrieves the details of a specific transfer.
Path Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
organization_id | string | Yes | The organization ID |
id | string | Yes | The transfer ID |
Response
Returns the complete transfer object with account details and transaction information.
Update Transfer
/organizations/:organization_id/transfers/:idUpdates an existing transfer.
Path Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
organization_id | string | Yes | The organization ID |
id | string | Yes | The transfer ID |
Request Body
| Parameter | Type | Description |
|---|---|---|
amount | string | Updated transfer amount |
transfer_date | date | Updated transfer date |
description | string | Updated description |
Delete Transfer
/organizations/:organization_id/transfers/:idDeletes an existing transfer and reverses the associated accounting transactions.
Path Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
organization_id | string | Yes | The organization ID |
id | string | Yes | The transfer ID |
Transfer Types
Cash Management Transfers
{
"transfers": [
{
"type": "operating_reserve",
"description": "Build operating cash reserve",
"from_account": "business_checking",
"to_account": "operating_reserve",
"frequency": "monthly",
"amount": "50000.00"
},
{
"type": "emergency_fund",
"description": "Emergency fund contribution",
"from_account": "business_checking",
"to_account": "emergency_fund",
"frequency": "monthly",
"amount": "10000.00"
},
{
"type": "tax_payment",
"description": "Transfer for quarterly tax payment",
"from_account": "business_checking",
"to_account": "tax_liability",
"frequency": "quarterly",
"amount": "variable"
}
]
}
Account Reclassification
{
"transfers": [
{
"type": "reclassification",
"description": "Reclassify expense from wrong category",
"from_account": "travel_expense",
"to_account": "meals_expense",
"amount": "450.00",
"reason": "Meal expense misclassified as travel"
}
]
}
Inter-Company Transfers
{
"transfers": [
{
"type": "intercompany",
"description": "Transfer from parent to subsidiary",
"from_account": "parent_company_checking",
"to_account": "subsidiary_checking",
"amount": "25000.00",
"metadata": {
"subsidiary_id": "org_sub_001",
"purpose": "working_capital"
}
}
]
}
Double-Entry Accounting
How Transfers Work
When you create a transfer, Crane Ledger automatically creates two balanced transactions:
Expense Transaction (reduces the source account):
- Debit: Destination Account (+)
- Credit: Source Account (-)
Income Transaction (increases the destination account):
- Debit: Source Account (+)
- Credit: Destination Account (-)
Example: Transfer from Checking to Savings
{
"transfer": {
"from_account_id": "act_checking",
"to_account_id": "act_savings",
"amount": "1000.00",
"transferred_at": "2024-01-15"
},
"resulting_transactions": [
{
"type": "expense",
"description": "Transfer to savings",
"entries": [
{
"account_id": "act_checking",
"entry_type": "credit",
"amount": "1000.00"
},
{
"account_id": "act_savings",
"entry_type": "debit",
"amount": "1000.00"
}
]
},
{
"type": "income",
"description": "Transfer from checking",
"entries": [
{
"account_id": "act_savings",
"entry_type": "credit",
"amount": "1000.00"
},
{
"account_id": "act_checking",
"entry_type": "debit",
"amount": "1000.00"
}
]
}
]
}
Transfer Validation Rules
Account Compatibility
function validateTransferAccounts(fromAccount, toAccount) {
const validations = {
differentAccounts: fromAccount.id !== toAccount.id,
sameCurrency: fromAccount.currency_id === toAccount.currency_id,
sameOrganization: fromAccount.organization_id === toAccount.organization_id,
activeAccounts: fromAccount.status === 'active' && toAccount.status === 'active',
validAccountTypes: true
};
// Check account type compatibility
const compatibleTypes = {
'asset': ['asset', 'liability'],
'liability': ['asset', 'liability', 'equity'],
'equity': ['liability', 'equity']
};
validations.validAccountTypes = compatibleTypes[fromAccount.type]?.includes(toAccount.type);
// Revenue and expense accounts typically don't receive transfers
if ([fromAccount.type, toAccount.type].includes('revenue') ||
[fromAccount.type, toAccount.type].includes('expense')) {
validations.validAccountTypes = false;
}
return {
isValid: Object.values(validations).every(v => v === true),
validations
};
}
// Usage
const validation = validateTransferAccounts(fromAccount, toAccount);
if (!validation.isValid) {
console.log('Invalid transfer:', validation.validations);
}
Amount Validation
def validate_transfer_amount(amount, from_account, to_account):
"""Validate transfer amount and account balances"""
validations = {
'positive_amount': amount > 0,
'sufficient_balance': True,
'reasonable_amount': amount < 10000000, # Business rule
'currency_match': from_account.currency_id == to_account.currency_id
}
# Check available balance for asset accounts
if from_account.type == 'asset':
available_balance = get_account_balance(from_account.id)
validations['sufficient_balance'] = available_balance >= amount
# Business rules
if amount > 500000: # Large transfers require approval
validations['requires_approval'] = True
return {
'is_valid': all(validations.values()),
'validations': validations
}
Transfer Workflows
Cash Management
def optimize_cash_position(accounts, target_ratios):
"""Optimize cash position across accounts"""
transfers = []
# Calculate current ratios
total_cash = sum(acc.balance for acc in accounts if acc.type == 'asset')
operating_balance = get_account_balance('operating_account')
reserve_balance = get_account_balance('reserve_account')
# Target operating balance (e.g., 30 days of expenses)
target_operating = calculate_target_operating_balance()
if operating_balance < target_operating:
# Transfer from reserve to operating
transfer_amount = min(
target_operating - operating_balance,
reserve_balance * 0.5 # Don't deplete reserve too much
)
if transfer_amount > 0:
transfers.append({
'from_account': 'reserve_account',
'to_account': 'operating_account',
'amount': transfer_amount,
'description': 'Cash position optimization'
})
elif operating_balance > target_operating * 1.2:
# Transfer excess to reserve
transfer_amount = operating_balance - target_operating
transfers.append({
'from_account': 'operating_account',
'to_account': 'reserve_account',
'amount': transfer_amount,
'description': 'Excess cash to reserve'
})
return transfers
Payroll Funding
// Transfer funds for payroll
async function fundPayroll(payrollAmount, payrollDate) {
const transfers = [];
// Check operating account balance
const operatingBalance = await getAccountBalance('operating_account');
if (operatingBalance < payrollAmount) {
const shortfall = payrollAmount - operatingBalance;
// Transfer from payroll reserve
transfers.push({
from_account_id: 'payroll_reserve',
to_account_id: 'operating_account',
amount: shortfall,
transferred_at: payrollDate,
description: `Payroll funding - ${payrollDate}`,
reference: `PAYROLL-${payrollDate.replace(/-/g, '')}`
});
}
// Execute transfers
for (const transfer of transfers) {
await createTransfer(transfer);
}
return {
transfers_executed: transfers.length,
total_transferred: transfers.reduce((sum, t) => sum + t.amount, 0)
};
}
Transfer Scheduling
Recurring Transfers
{
"scheduled_transfers": [
{
"id": "sched_monthly_reserve",
"description": "Monthly operating reserve build",
"from_account_id": "act_operating",
"to_account_id": "act_reserve",
"amount": "25000.00",
"schedule": {
"frequency": "monthly",
"day_of_month": 1,
"start_date": "2024-01-01",
"end_date": "2024-12-31"
},
"auto_execute": true,
"conditions": {
"minimum_balance": "75000.00"
}
}
]
}
Conditional Transfers
def execute_conditional_transfer(transfer_config):
"""Execute transfer based on conditions"""
conditions = transfer_config['conditions']
# Check balance condition
if 'minimum_balance' in conditions:
from_balance = get_account_balance(transfer_config['from_account_id'])
if from_balance < conditions['minimum_balance']:
return {'status': 'skipped', 'reason': 'insufficient_balance'}
# Check date conditions
if 'execute_on_day' in conditions:
today = datetime.now().day
if today != conditions['execute_on_day']:
return {'status': 'skipped', 'reason': 'wrong_day'}
# Execute transfer
return create_transfer(transfer_config)
Transfer Reporting
Transfer History
-- Get transfer history for period
SELECT
t.id,
t.transferred_at,
fa.name as from_account,
ta.name as to_account,
t.amount,
t.description,
t.reference
FROM transfers t
JOIN accounts fa ON t.from_account_id = fa.id
JOIN accounts ta ON t.to_account_id = ta.id
WHERE t.organization_id = $1
AND t.transferred_at BETWEEN $2 AND $3
ORDER BY t.transferred_at DESC;
Cash Flow Analysis
def analyze_transfer_impact(transfers, accounts):
"""Analyze how transfers affect cash flow"""
impact = {
'net_cash_movement': 0,
'accounts_affected': set(),
'cash_positions': {},
'liquidity_changes': {}
}
for transfer in transfers:
from_acc = accounts[transfer['from_account_id']]
to_acc = accounts[transfer['to_account_id']]
# Track affected accounts
impact['accounts_affected'].update([transfer['from_account_id'], transfer['to_account_id']])
# Calculate cash impact
if from_acc.type == 'asset':
impact['net_cash_movement'] -= transfer['amount']
if to_acc.type == 'asset':
impact['net_cash_movement'] += transfer['amount']
# Update account positions
impact['cash_positions'][transfer['from_account_id']] = \
impact['cash_positions'].get(transfer['from_account_id'], 0) - transfer['amount']
impact['cash_positions'][transfer['to_account_id']] = \
impact['cash_positions'].get(transfer['to_account_id'], 0) + transfer['amount']
return impact
Transfer Best Practices
Cash Management
- Maintain Operating Reserves: Keep 3-6 months of expenses in liquid reserves
- Optimize Interest: Move excess cash to high-yield accounts
- Seasonal Planning: Build cash reserves before seasonal slowdowns
Internal Controls
- Approval Workflows: Require approval for large transfers
- Dual Authorization: Two people required for significant transfers
- Audit Trail: Complete documentation for all transfers
Accounting Accuracy
- Proper Classification: Use correct transfer dates
- Consistent Descriptions: Clear, searchable transfer descriptions
- Regular Reconciliation: Verify transfer posting accuracy
Transfer Reversibility
Transfers create permanent accounting entries. To reverse a transfer, create a new transfer in the opposite direction with appropriate documentation.
Need help?
Create a free account to access our support portal. Once signed in, use the Support tab in your dashboard to submit a support ticket — our team typically responds within 24 hours.
- ✨ For LLMs/AI assistants: Read our structured API reference