List Balance Transfers
Retrieve a list of balance_transfer resources for your application. This endpoint allows you to view the history of fund movements between internal accounts and track the status of all balance transfers.
Use Cases
1. List Recent Balance Transfers
Retrieve the most recent balance transfers with default pagination:
curl https://api.ahrvo.network/payments/na/balance_transfers \
-u username:password \
-H "Content-Type: application/json"
2. List All Transfers with Custom Pagination
Retrieve transfers with custom page size:
curl "https://api.ahrvo.network/payments/na/balance_transfers?limit=50&offset=0" \
-u username:password \
-H "Content-Type: application/json"
3. Sort Transfers by Creation Date
Get transfers sorted by creation date in descending order (newest first):
curl "https://api.ahrvo.network/payments/na/balance_transfers?sort=created_at,desc&limit=100" \
-u username:password \
-H "Content-Type: application/json"
4. Paginate Through All Transfers
Navigate through large result sets using offset:
async function getAllBalanceTransfers() {
const allTransfers = [];
let offset = 0;
const limit = 100;
let hasMore = true;
while (hasMore) {
const response = await fetch(
`https://api.ahrvo.network/payments/na/balance_transfers?limit=${limit}&offset=${offset}`,
{
headers: {
'Authorization': 'Basic ' + btoa('username:password'),
'Content-Type': 'application/json'
}
}
);
const data = await response.json();
const transfers = data._embedded?.balance_transfers || [];
allTransfers.push(...transfers);
// Check if there are more results
hasMore = transfers.length === limit;
offset += limit;
console.log(`Fetched ${transfers.length} transfers, total: ${allTransfers.length}`);
}
return allTransfers;
}
5. Filter Failed Transfers
Find all failed transfers for investigation:
async function getFailedTransfers() {
const response = await fetch(
'https://api.ahrvo.network/payments/na/balance_transfers?limit=100',
{
headers: {
'Authorization': 'Basic ' + btoa('username:password'),
'Content-Type': 'application/json'
}
}
);
const data = await response.json();
const allTransfers = data._embedded?.balance_transfers || [];
// Filter client-side for failed transfers
const failedTransfers = allTransfers.filter(t => t.state === 'FAILED');
console.log(`Found ${failedTransfers.length} failed transfers`);
// Analyze failure patterns
failedTransfers.forEach(transfer => {
console.log('Failed Transfer:', {
id: transfer.id,
amount: transfer.amount,
source: transfer.source,
destination: transfer.destination,
created_at: transfer.created_at,
description: transfer.description
});
});
return failedTransfers;
}
6. Calculate Transfer Statistics
Generate statistics from transfer history:
async function calculateTransferStats() {
const allTransfers = await getAllBalanceTransfers();
const stats = {
total_count: allTransfers.length,
total_amount: 0,
succeeded_count: 0,
succeeded_amount: 0,
failed_count: 0,
failed_amount: 0,
pending_count: 0,
pending_amount: 0,
to_fbo_count: 0,
to_fbo_amount: 0,
from_fbo_count: 0,
from_fbo_amount: 0
};
allTransfers.forEach(transfer => {
stats.total_amount += transfer.amount;
// By state
if (transfer.state === 'SUCCEEDED') {
stats.succeeded_count++;
stats.succeeded_amount += transfer.amount;
} else if (transfer.state === 'FAILED') {
stats.failed_count++;
stats.failed_amount += transfer.amount;
} else if (transfer.state === 'PENDING') {
stats.pending_count++;
stats.pending_amount += transfer.amount;
}
// By direction
if (transfer.destination === 'FOR_BENEFIT_OF_ACCOUNT') {
stats.to_fbo_count++;
stats.to_fbo_amount += transfer.amount;
} else if (transfer.source === 'FOR_BENEFIT_OF_ACCOUNT') {
stats.from_fbo_count++;
stats.from_fbo_amount += transfer.amount;
}
});
// Calculate success rate
stats.success_rate = (stats.succeeded_count / stats.total_count * 100).toFixed(2);
// Calculate net FBO flow
stats.net_fbo_flow = stats.to_fbo_amount - stats.from_fbo_amount;
return stats;
}
7. Generate Monthly Transfer Report
Create a comprehensive monthly report:
async function generateMonthlyReport(year, month) {
const allTransfers = await getAllBalanceTransfers();
// Filter transfers for the specified month
const monthlyTransfers = allTransfers.filter(transfer => {
const date = new Date(transfer.created_at);
return date.getFullYear() === year && date.getMonth() === month - 1;
});
const report = {
period: `${year}-${String(month).padStart(2, '0')}`,
total_transfers: monthlyTransfers.length,
transfers_by_state: {
succeeded: 0,
failed: 0,
pending: 0
},
amounts_by_state: {
succeeded: 0,
failed: 0,
pending: 0
},
transfers_by_direction: {
to_fbo: 0,
from_fbo: 0
},
amounts_by_direction: {
to_fbo: 0,
from_fbo: 0
},
daily_breakdown: {}
};
monthlyTransfers.forEach(transfer => {
const date = new Date(transfer.created_at).toISOString().split('T')[0];
// By state
const state = transfer.state.toLowerCase();
report.transfers_by_state[state]++;
report.amounts_by_state[state] += transfer.amount;
// By direction
if (transfer.destination === 'FOR_BENEFIT_OF_ACCOUNT') {
report.transfers_by_direction.to_fbo++;
report.amounts_by_direction.to_fbo += transfer.amount;
} else {
report.transfers_by_direction.from_fbo++;
report.amounts_by_direction.from_fbo += transfer.amount;
}
// Daily breakdown
if (!report.daily_breakdown[date]) {
report.daily_breakdown[date] = {
count: 0,
amount: 0,
to_fbo: 0,
from_fbo: 0
};
}
report.daily_breakdown[date].count++;
report.daily_breakdown[date].amount += transfer.amount;
if (transfer.destination === 'FOR_BENEFIT_OF_ACCOUNT') {
report.daily_breakdown[date].to_fbo += transfer.amount;
} else {
report.daily_breakdown[date].from_fbo += transfer.amount;
}
});
// Calculate net flow
report.net_fbo_flow = report.amounts_by_direction.to_fbo - report.amounts_by_direction.from_fbo;
return report;
}
8. Export Transfers to CSV
Export transfer history to CSV format:
async function exportTransfersToCSV() {
const allTransfers = await getAllBalanceTransfers();
// CSV header
const headers = [
'ID',
'Created At',
'Amount',
'Currency',
'Source',
'Destination',
'State',
'Description',
'Reference ID',
'Processor Type'
];
// Build CSV rows
const rows = allTransfers.map(transfer => [
transfer.id,
transfer.created_at,
(transfer.amount / 100).toFixed(2), // Convert cents to dollars
transfer.currency,
transfer.source,
transfer.destination,
transfer.state,
`"${transfer.description || ''}"`, // Quote description to handle commas
transfer.reference_id,
transfer.processor_type
]);
// Combine header and rows
const csv = [
headers.join(','),
...rows.map(row => row.join(','))
].join('\n');
// Save to file or return
return csv;
}
9. Monitor Pending Transfers
Track pending transfers and check their status:
async function monitorPendingTransfers() {
const response = await fetch(
'https://api.ahrvo.network/payments/na/balance_transfers?limit=100',
{
headers: {
'Authorization': 'Basic ' + btoa('username:password'),
'Content-Type': 'application/json'
}
}
);
const data = await response.json();
const allTransfers = data._embedded?.balance_transfers || [];
// Find pending transfers
const pendingTransfers = allTransfers.filter(t => t.state === 'PENDING');
if (pendingTransfers.length === 0) {
console.log('No pending transfers');
return [];
}
console.log(`Found ${pendingTransfers.length} pending transfers`);
// Check age of pending transfers
const now = Date.now();
const alertThreshold = 30 * 60 * 1000; // 30 minutes
const stalePending = pendingTransfers.filter(transfer => {
const age = now - new Date(transfer.created_at).getTime();
return age > alertThreshold;
});
if (stalePending.length > 0) {
console.warn(`ALERT: ${stalePending.length} transfers pending for over 30 minutes`);
stalePending.forEach(transfer => {
const ageMinutes = Math.floor((now - new Date(transfer.created_at).getTime()) / 60000);
console.warn(`Transfer ${transfer.id} pending for ${ageMinutes} minutes`);
});
// Send alert
await sendAlert('Stale pending transfers detected', stalePending);
}
return pendingTransfers;
}
10. Find Transfers by Tag
Search for transfers with specific tags:
async function findTransfersByTag(tagKey, tagValue) {
const allTransfers = await getAllBalanceTransfers();
// Filter transfers by tag
const matchingTransfers = allTransfers.filter(transfer => {
return transfer.tags && transfer.tags[tagKey] === tagValue;
});
console.log(`Found ${matchingTransfers.length} transfers with tag ${tagKey}=${tagValue}`);
return matchingTransfers;
}
// Example usage:
// Find all daily reconciliation transfers
const reconciliationTransfers = await findTransfersByTag('purpose', 'daily_reconciliation');
// Find transfers from a specific date
const dateTransfers = await findTransfersByTag('date', '2025-12-10');
Query Parameters
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
limit | integer | No | 20 | Number of items to return (max typically 100) |
offset | integer | No | 0 | Number of items to skip before returning results |
sort | string | No | - | Sort order (e.g., "created_at,desc" or "amount,asc") |
Response Structure
The response contains:
_embedded.balance_transfers: Array of balance transfer objects_links: HATEOAS navigation linkspage: Pagination informationoffset: Current offsetlimit: Items per pagecount: Total number of items in this response
Transfer Fields
Each balance transfer in the array contains:
| Field | Type | Description |
|---|---|---|
id | string | Unique identifier for the balance transfer |
created_at | string | ISO 8601 timestamp of creation |
updated_at | string | ISO 8601 timestamp of last update |
amount | integer | Amount transferred in cents |
currency | string | 3-letter ISO 4217 currency code |
description | string | Transfer description |
source | string | Source account type |
destination | string | Destination account type |
state | string | Transfer state: SUCCEEDED, FAILED, or PENDING |
processor_type | string | Processor used for the transfer |
reference_id | string | Internal reference ID |
external_reference_id | string | External reference ID (if any) |
tags | object | Custom metadata |
Best Practices
1. Use Pagination Wisely
Don't fetch more data than you need:
// Good: Fetch only what you need
async function getRecentTransfers(count = 20) {
const response = await fetch(
`https://api.ahrvo.network/payments/na/balance_transfers?limit=${count}&sort=created_at,desc`,
{
headers: {
'Authorization': 'Basic ' + btoa('username:password')
}
}
);
const data = await response.json();
return data._embedded?.balance_transfers || [];
}
// Bad: Always fetching maximum
// Don't do this unless you really need all transfers
2. Cache Appropriately
Cache transfer lists when appropriate to reduce API calls:
class BalanceTransferCache {
constructor(ttl = 5 * 60 * 1000) { // 5 minute default TTL
this.cache = null;
this.cacheTime = null;
this.ttl = ttl;
}
async getTransfers(forceRefresh = false) {
const now = Date.now();
if (!forceRefresh && this.cache && (now - this.cacheTime) < this.ttl) {
console.log('Returning cached transfers');
return this.cache;
}
console.log('Fetching fresh transfers');
const response = await fetch(
'https://api.ahrvo.network/payments/na/balance_transfers?limit=100',
{
headers: {
'Authorization': 'Basic ' + btoa('username:password')
}
}
);
const data = await response.json();
this.cache = data._embedded?.balance_transfers || [];
this.cacheTime = now;
return this.cache;
}
invalidate() {
this.cache = null;
this.cacheTime = null;
}
}
const transferCache = new BalanceTransferCache();
3. Handle Empty Results
Always check for empty result sets:
async function getTransfers() {
const response = await fetch(
'https://api.ahrvo.network/payments/na/balance_transfers',
{
headers: {
'Authorization': 'Basic ' + btoa('username:password')
}
}
);
const data = await response.json();
const transfers = data._embedded?.balance_transfers || [];
if (transfers.length === 0) {
console.log('No balance transfers found');
return [];
}
console.log(`Found ${transfers.length} transfers`);
return transfers;
}
4. Filter Client-Side
Since the API doesn't support filtering by state or account type, filter on the client:
async function getTransfersByState(state) {
const allTransfers = await getTransfers();
return allTransfers.filter(t => t.state === state);
}
async function getTransfersToFBO() {
const allTransfers = await getTransfers();
return allTransfers.filter(t => t.destination === 'FOR_BENEFIT_OF_ACCOUNT');
}
async function getTransfersFromFBO() {
const allTransfers = await getTransfers();
return allTransfers.filter(t => t.source === 'FOR_BENEFIT_OF_ACCOUNT');
}
5. Track Net Flow
Calculate net fund movement:
async function calculateNetFlow() {
const allTransfers = await getAllBalanceTransfers();
const succeededTransfers = allTransfers.filter(t => t.state === 'SUCCEEDED');
const toFBO = succeededTransfers
.filter(t => t.destination === 'FOR_BENEFIT_OF_ACCOUNT')
.reduce((sum, t) => sum + t.amount, 0);
const fromFBO = succeededTransfers
.filter(t => t.source === 'FOR_BENEFIT_OF_ACCOUNT')
.reduce((sum, t) => sum + t.amount, 0);
const netFlow = toFBO - fromFBO;
return {
total_to_fbo: toFBO,
total_from_fbo: fromFBO,
net_flow: netFlow,
net_flow_formatted: `$${(Math.abs(netFlow) / 100).toFixed(2)} ${netFlow >= 0 ? 'to FBO' : 'from FBO'}`
};
}
6. Set Up Automated Reporting
Generate regular reports from transfer history:
async function dailyTransferReport() {
const today = new Date().toISOString().split('T')[0];
const allTransfers = await getAllBalanceTransfers();
// Filter today's transfers
const todayTransfers = allTransfers.filter(transfer => {
return transfer.created_at.startsWith(today);
});
const report = {
date: today,
total_count: todayTransfers.length,
total_amount: todayTransfers.reduce((sum, t) => sum + t.amount, 0),
succeeded: todayTransfers.filter(t => t.state === 'SUCCEEDED').length,
failed: todayTransfers.filter(t => t.state === 'FAILED').length,
pending: todayTransfers.filter(t => t.state === 'PENDING').length,
to_fbo_amount: todayTransfers
.filter(t => t.destination === 'FOR_BENEFIT_OF_ACCOUNT')
.reduce((sum, t) => sum + t.amount, 0),
from_fbo_amount: todayTransfers
.filter(t => t.source === 'FOR_BENEFIT_OF_ACCOUNT')
.reduce((sum, t) => sum + t.amount, 0)
};
console.log('Daily Transfer Report:', report);
// Send report via email or notification
await sendDailyReport(report);
return report;
}
// Schedule daily at midnight
// scheduleCronJob('0 0 * * *', dailyTransferReport);
Common Workflows
Reconciliation Workflow
async function reconcileBalanceTransfers() {
// 1. Get all successful transfers
const allTransfers = await getAllBalanceTransfers();
const successfulTransfers = allTransfers.filter(t => t.state === 'SUCCEEDED');
// 2. Calculate total movements
const toFBO = successfulTransfers
.filter(t => t.destination === 'FOR_BENEFIT_OF_ACCOUNT')
.reduce((sum, t) => sum + t.amount, 0);
const fromFBO = successfulTransfers
.filter(t => t.source === 'FOR_BENEFIT_OF_ACCOUNT')
.reduce((sum, t) => sum + t.amount, 0);
// 3. Get current account balances
const operatingBalance = await getAccountBalance('OPERATING_ACCOUNT');
const fboBalance = await getAccountBalance('FOR_BENEFIT_OF_ACCOUNT');
// 4. Compare with expected balances
console.log('Reconciliation Results:', {
total_to_fbo: toFBO,
total_from_fbo: fromFBO,
net_fbo_change: toFBO - fromFBO,
current_operating_balance: operatingBalance,
current_fbo_balance: fboBalance
});
// 5. Flag discrepancies
const netChange = toFBO - fromFBO;
const tolerance = 100; // $1.00 tolerance
if (Math.abs(netChange - (fboBalance - previousFBOBalance)) > tolerance) {
console.warn('RECONCILIATION DISCREPANCY DETECTED');
await alertFinanceTeam('Balance transfer reconciliation mismatch');
}
}
Audit Trail Report
async function generateAuditTrail(startDate, endDate) {
const allTransfers = await getAllBalanceTransfers();
// Filter by date range
const transfers = allTransfers.filter(transfer => {
const date = new Date(transfer.created_at);
return date >= startDate && date <= endDate;
});
// Group by day
const byDay = {};
transfers.forEach(transfer => {
const day = transfer.created_at.split('T')[0];
if (!byDay[day]) {
byDay[day] = [];
}
byDay[day].push(transfer);
});
// Generate detailed report
const auditReport = {
period: {
start: startDate.toISOString(),
end: endDate.toISOString()
},
summary: {
total_transfers: transfers.length,
total_amount: transfers.reduce((sum, t) => sum + t.amount, 0),
success_count: transfers.filter(t => t.state === 'SUCCEEDED').length,
failure_count: transfers.filter(t => t.state === 'FAILED').length
},
daily_details: byDay
};
return auditReport;
}