Skip to main content

Top Up Budget Account

Overview

Use this endpoint to transfer funds and top up the balance of a specific budget account. Budget accounts serve as funding sources for card operations, and maintaining adequate balance is essential for uninterrupted card spending.

NOTE: This API is offered on a client-by-client approval basis. Speak to your account manager to check eligibility.

Resource Access

Production (api.ahrvo.network)

POST https://api.ahrvo.network/card/issuance/api/issuing/budget/v2/top-up

Staging (gateway.ahrvo.network)

POST https://gateway.ahrvo.network/card/issuance/api/issuing/budget/v2/top-up

Request Headers

HeaderValueRequiredDescription
Acceptapplication/jsonYesContent type for the response
Content-Typeapplication/jsonYesContent type of the request body
AuthorizationBearer {access_token}YesBearer token for authentication
x-api-keyAPI KeyYesAPI key for authentication

Request Body

FieldTypeRequiredDescription
budget_idstringYesA unique ID of the budget account to top up
top_up_amountstringYesThe amount of funds to be transferred into the budget. Must be a positive number with up to 2 decimal places
top_up_currencystringYesCurrency of the top-up amount. Must follow ISO 4217 standard (e.g., USD, EUR, GBP)
unique_order_idstringYesTop-up order ID provided by you for idempotency. Use this to safely retry requests and query the status later

Request Example

{
"budget_id": "ci201910231405089666",
"top_up_amount": "10.45",
"top_up_currency": "USD",
"unique_order_id": "1621924039"
}

Response

Success Response (200 OK)

{
"code": "SUCCESS",
"data": {
"record_id": "example_record_id"
}
}

Response Fields

FieldTypeDescription
codestringStatus string indicating the result of the request. "SUCCESS" refers to a successful initiation
dataobjectResponse data object
data.record_idstringA unique ID of the top-up record generated by the system

Error Responses

  • 400 Bad Request: Invalid parameters, invalid amount format, or duplicate unique_order_id
  • 401 Unauthorized: Invalid or missing authentication token
  • 403 Forbidden: Card issuance feature not enabled for this account
  • 404 Not Found: Budget account does not exist

Code Examples

cURL

curl -X POST \
https://gateway.ahrvo.network/card/issuance/api/issuing/budget/v2/top-up \
-H 'Accept: application/json' \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer YOUR_ACCESS_TOKEN' \
-H 'x-api-key: YOUR_API_KEY' \
-d '{
"budget_id": "ci201910231405089666",
"top_up_amount": "10.45",
"top_up_currency": "USD",
"unique_order_id": "1621924039"
}'

Python

import requests
import time

url = "https://gateway.ahrvo.network/card/issuance/api/issuing/budget/v2/top-up"
headers = {
"Accept": "application/json",
"Content-Type": "application/json",
"Authorization": "Bearer YOUR_ACCESS_TOKEN",
"x-api-key": "YOUR_API_KEY"
}
payload = {
"budget_id": "ci201910231405089666",
"top_up_amount": "10.45",
"top_up_currency": "USD",
"unique_order_id": f"topup_{int(time.time())}" # Generate unique ID
}

response = requests.post(url, headers=headers, json=payload)
result = response.json()

if result['code'] == 'SUCCESS':
record_id = result['data']['record_id']
unique_order_id = payload['unique_order_id']

print(f"✓ Top-up initiated successfully")
print(f" Record ID: {record_id}")
print(f" Order ID: {unique_order_id}")
print(f"\nUse the order ID to check status with the Query Top-Up Order endpoint")
else:
print(f"Failed to initiate top-up: {result}")

JavaScript (Node.js)

const axios = require('axios');

const url = 'https://gateway.ahrvo.network/card/issuance/api/issuing/budget/v2/top-up';
const headers = {
'Accept': 'application/json',
'Content-Type': 'application/json',
'Authorization': 'Bearer YOUR_ACCESS_TOKEN',
'x-api-key': 'YOUR_API_KEY'
};
const payload = {
budget_id: 'ci201910231405089666',
top_up_amount: '10.45',
top_up_currency: 'USD',
unique_order_id: `topup_${Date.now()}` // Generate unique ID
};

axios.post(url, payload, { headers })
.then(response => {
const result = response.data;

if (result.code === 'SUCCESS') {
console.log('✓ Top-up initiated successfully');
console.log(` Record ID: ${result.data.record_id}`);
console.log(` Order ID: ${payload.unique_order_id}`);
console.log('\nUse the order ID to check status with the Query Top-Up Order endpoint');
}
})
.catch(error => {
console.error('Failed to initiate top-up:', error.response.data);
});

Usage Notes

  • Asynchronous Processing: Top-ups are processed asynchronously. The successful response means the request was accepted, not that funds are immediately available
  • Status Verification: Use the Query Top-Up Order endpoint with unique_order_id to check if the top-up succeeded
  • Unique Order ID: Generate unique IDs (e.g., timestamp + UUID) to prevent duplicates
  • Idempotency: Using the same unique_order_id in retries prevents duplicate top-ups
  • Amount Format: Provide amounts as strings with up to 2 decimal places (e.g., "10.45", not 10.45)
  • Currency: Must match the budget account's currency

Common Use Cases

Basic Top-Up with Status Check

Initiate a top-up and verify it completed successfully:

import time

def top_up_budget(budget_id, amount, currency='USD'):
"""
Top up a budget account and wait for completion
"""
# Generate unique order ID
unique_order_id = f"topup_{int(time.time())}"

# Initiate top-up
print(f"Initiating top-up of {currency} {amount}...")
response = initiate_top_up(
budget_id=budget_id,
amount=amount,
currency=currency,
unique_order_id=unique_order_id
)

if response['code'] == 'SUCCESS':
record_id = response['data']['record_id']
print(f"✓ Top-up initiated (Record ID: {record_id})")

# Poll for completion
print("Checking top-up status...")
max_attempts = 10

for attempt in range(max_attempts):
time.sleep(3) # Wait 3 seconds between checks

status_response = query_top_up_order(unique_order_id)

if status_response['code'] == 'SUCCESS':
status = status_response['data']['status']

if status == 'SUCCESS':
print(f"✓ Top-up completed successfully")
return {
'success': True,
'record_id': record_id,
'unique_order_id': unique_order_id
}
elif status == 'FAILED':
print(f"✗ Top-up failed")
return {
'success': False,
'unique_order_id': unique_order_id
}
elif status == 'PROCESSING':
print(f"⏳ Attempt {attempt + 1}/{max_attempts}: Still processing...")

print("⚠ Top-up status check timed out")
return {
'success': None,
'unique_order_id': unique_order_id,
'error': 'Timeout'
}
else:
print(f"✗ Failed to initiate top-up: {response}")
return {'success': False, 'error': response}

# Example usage
result = top_up_budget('ci201910231405089666', '1000.00')

Automatic Low Balance Top-Up

Monitor budget balance and automatically top up when low:

async function autoTopUpOnLowBalance(budgetId, threshold, topUpAmount) {
try {
// Check current balance
const balanceResponse = await queryBudgetBalance(budgetId);
const currentBalance = parseFloat(balanceResponse.data.balance);
const currency = balanceResponse.data.currency;

console.log(`Current balance: ${currency} ${currentBalance}`);

if (currentBalance < threshold) {
console.log(`⚠ Balance below threshold (${threshold})`);
console.log(`Initiating automatic top-up of ${currency} ${topUpAmount}...`);

const uniqueOrderId = `auto_topup_${Date.now()}`;

const topUpResponse = await topUpBudgetAccount({
budget_id: budgetId,
top_up_amount: topUpAmount.toFixed(2),
top_up_currency: currency,
unique_order_id: uniqueOrderId
});

if (topUpResponse.code === 'SUCCESS') {
console.log('✓ Auto top-up initiated');
console.log(` Record ID: ${topUpResponse.data.record_id}`);

// Wait for completion
let attempts = 0;
const maxAttempts = 10;

while (attempts < maxAttempts) {
await new Promise(resolve => setTimeout(resolve, 3000));
attempts++;

const statusResponse = await queryTopUpOrder(uniqueOrderId);

if (statusResponse.data.status === 'SUCCESS') {
console.log('✓ Auto top-up completed successfully');

// Verify new balance
const newBalanceResponse = await queryBudgetBalance(budgetId);
const newBalance = parseFloat(newBalanceResponse.data.balance);
console.log(`New balance: ${currency} ${newBalance}`);

return {
success: true,
old_balance: currentBalance,
new_balance: newBalance,
topped_up_amount: topUpAmount
};
} else if (statusResponse.data.status === 'FAILED') {
console.log('✗ Auto top-up failed');
return { success: false, error: 'Top-up failed' };
}

console.log(`⏳ Attempt ${attempts}/${maxAttempts}: Still processing...`);
}

console.log('⚠ Auto top-up status check timed out');
return { success: null, error: 'Timeout' };
}
} else {
console.log('✓ Balance is sufficient, no top-up needed');
return {
success: true,
balance_sufficient: true,
current_balance: currentBalance
};
}
} catch (error) {
console.error('Auto top-up failed:', error);
throw error;
}
}

// Example usage: Monitor every hour
setInterval(async () => {
await autoTopUpOnLowBalance(
'ci201910231405089666',
1000.00, // Threshold
5000.00 // Top-up amount
);
}, 3600000); // Check every hour

Scheduled Bulk Top-Ups

Top up multiple budget accounts at scheduled intervals:

def scheduled_bulk_top_up(budget_configs):
"""
Top up multiple budget accounts in bulk

Args:
budget_configs: List of dicts with budget_id, amount, currency
"""
import time
from datetime import datetime

results = {
'initiated': [],
'failed': [],
'timestamp': datetime.now().isoformat()
}

print(f"\n=== Starting Bulk Top-Up at {results['timestamp']} ===")
print(f"Processing {len(budget_configs)} budgets...")

for config in budget_configs:
budget_id = config['budget_id']
amount = config['amount']
currency = config.get('currency', 'USD')
budget_name = config.get('name', budget_id)

try:
# Generate unique order ID
unique_order_id = f"bulk_topup_{int(time.time())}_{budget_id[-6:]}"

print(f"\nTopping up {budget_name}...")
response = initiate_top_up(
budget_id=budget_id,
amount=str(amount),
currency=currency,
unique_order_id=unique_order_id
)

if response['code'] == 'SUCCESS':
record_id = response['data']['record_id']
print(f" ✓ Initiated: {currency} {amount} (Record: {record_id})")

results['initiated'].append({
'budget_id': budget_id,
'budget_name': budget_name,
'amount': amount,
'currency': currency,
'unique_order_id': unique_order_id,
'record_id': record_id
})
else:
print(f" ✗ Failed: {response}")
results['failed'].append({
'budget_id': budget_id,
'budget_name': budget_name,
'error': response
})

# Small delay between requests
time.sleep(1)

except Exception as e:
print(f" ✗ Error: {e}")
results['failed'].append({
'budget_id': budget_id,
'budget_name': budget_name,
'error': str(e)
})

# Summary
print(f"\n=== Bulk Top-Up Summary ===")
print(f"Successfully initiated: {len(results['initiated'])}")
print(f"Failed: {len(results['failed'])}")

# Verify all initiated top-ups
if results['initiated']:
print("\nVerifying top-up statuses...")
time.sleep(5) # Wait for processing

for item in results['initiated']:
status_response = query_top_up_order(item['unique_order_id'])
status = status_response['data']['status']
status_icon = '✓' if status == 'SUCCESS' else '✗' if status == 'FAILED' else '⏳'
print(f" {status_icon} {item['budget_name']}: {status}")

return results

# Example usage
budgets_to_top_up = [
{
'budget_id': 'ci201910231405089666',
'name': 'Marketing Budget',
'amount': 10000.00,
'currency': 'USD'
},
{
'budget_id': 'ci201910231405089667',
'name': 'Operations Budget',
'amount': 15000.00,
'currency': 'USD'
},
{
'budget_id': 'ci201910231405089668',
'name': 'Travel Budget',
'amount': 5000.00,
'currency': 'USD'
}
]

results = scheduled_bulk_top_up(budgets_to_top_up)

Top-Up with Retry on Failure

Implement retry logic for failed top-ups:

async function topUpWithRetry(budgetId, amount, currency, maxRetries = 3) {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
const uniqueOrderId = `topup_retry_${Date.now()}_${attempt}`;

console.log(`\nAttempt ${attempt}/${maxRetries}: Initiating top-up...`);

// Initiate top-up
const topUpResponse = await topUpBudgetAccount({
budget_id: budgetId,
top_up_amount: amount,
top_up_currency: currency,
unique_order_id: uniqueOrderId
});

if (topUpResponse.code === 'SUCCESS') {
console.log(`✓ Top-up initiated (Record: ${topUpResponse.data.record_id})`);

// Poll for status
let statusAttempts = 0;
const maxStatusAttempts = 10;

while (statusAttempts < maxStatusAttempts) {
await new Promise(resolve => setTimeout(resolve, 3000));
statusAttempts++;

const statusResponse = await queryTopUpOrder(uniqueOrderId);
const status = statusResponse.data.status;

if (status === 'SUCCESS') {
console.log(`✓ Top-up completed successfully on attempt ${attempt}`);
return {
success: true,
unique_order_id: uniqueOrderId,
attempts: attempt
};
} else if (status === 'FAILED') {
console.log(`✗ Top-up failed on attempt ${attempt}`);

if (attempt < maxRetries) {
console.log(`Retrying... (${maxRetries - attempt} attempts remaining)`);
await new Promise(resolve => setTimeout(resolve, 5000)); // Wait before retry
break; // Exit status check loop to retry
} else {
return {
success: false,
unique_order_id: uniqueOrderId,
attempts: attempt,
error: 'Max retries exceeded'
};
}
} else if (status === 'PROCESSING') {
console.log(`⏳ Status check ${statusAttempts}/${maxStatusAttempts}: Still processing...`);
}
}
} else {
console.log(`✗ Failed to initiate top-up: ${topUpResponse}`);

if (attempt < maxRetries) {
console.log(`Retrying... (${maxRetries - attempt} attempts remaining)`);
await new Promise(resolve => setTimeout(resolve, 5000));
}
}
} catch (error) {
console.error(`Error on attempt ${attempt}:`, error.message);

if (attempt < maxRetries) {
console.log(`Retrying... (${maxRetries - attempt} attempts remaining)`);
await new Promise(resolve => setTimeout(resolve, 5000));
} else {
return {
success: false,
attempts: attempt,
error: error.message
};
}
}
}

return {
success: false,
attempts: maxRetries,
error: 'Max retries exceeded'
};
}

// Example usage
const result = await topUpWithRetry(
'ci201910231405089666',
'5000.00',
'USD',
3
);

Top-Up Alert Notifications

Send notifications when top-ups complete or fail:

def top_up_with_notifications(budget_id, amount, currency, notification_email):
"""
Top up budget and send email notifications on completion
"""
import time
from datetime import datetime

unique_order_id = f"topup_{int(time.time())}"
start_time = datetime.now()

try:
# Initiate top-up
print(f"Initiating top-up: {currency} {amount}")
response = initiate_top_up(
budget_id=budget_id,
amount=str(amount),
currency=currency,
unique_order_id=unique_order_id
)

if response['code'] != 'SUCCESS':
send_notification(
to=notification_email,
subject='Top-Up Initiation Failed',
body=f'Failed to initiate top-up for budget {budget_id}\nError: {response}'
)
return {'success': False, 'error': 'Initiation failed'}

record_id = response['data']['record_id']
print(f"Top-up initiated (Record: {record_id})")

# Monitor status
max_attempts = 10
for attempt in range(max_attempts):
time.sleep(3)

status_response = query_top_up_order(unique_order_id)
status = status_response['data']['status']

if status == 'SUCCESS':
end_time = datetime.now()
duration = (end_time - start_time).total_seconds()

# Send success notification
send_notification(
to=notification_email,
subject='✓ Top-Up Completed Successfully',
body=f'''
Budget Top-Up Successful

Budget ID: {budget_id}
Amount: {currency} {amount}
Record ID: {record_id}
Order ID: {unique_order_id}
Completed in: {duration:.1f} seconds
Time: {end_time.strftime('%Y-%m-%d %H:%M:%S')}
'''
)

print(f"✓ Top-up completed successfully (Duration: {duration:.1f}s)")
return {
'success': True,
'record_id': record_id,
'unique_order_id': unique_order_id,
'duration_seconds': duration
}

elif status == 'FAILED':
# Send failure notification
send_notification(
to=notification_email,
subject='✗ Top-Up Failed',
body=f'''
Budget Top-Up Failed

Budget ID: {budget_id}
Amount: {currency} {amount}
Record ID: {record_id}
Order ID: {unique_order_id}
Time: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}

Please check your funding source and retry.
'''
)

print(f"✗ Top-up failed")
return {
'success': False,
'unique_order_id': unique_order_id,
'error': 'Top-up failed'
}

print(f"⏳ Attempt {attempt + 1}/{max_attempts}: Status {status}")

# Timeout notification
send_notification(
to=notification_email,
subject='⚠ Top-Up Status Unknown',
body=f'''
Budget Top-Up Status Check Timed Out

Budget ID: {budget_id}
Amount: {currency} {amount}
Order ID: {unique_order_id}

Please check the status manually.
'''
)

return {
'success': None,
'unique_order_id': unique_order_id,
'error': 'Timeout'
}

except Exception as e:
send_notification(
to=notification_email,
subject='✗ Top-Up Error',
body=f'''
Budget Top-Up Error

Budget ID: {budget_id}
Amount: {currency} {amount}
Error: {str(e)}
Time: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
'''
)
raise

# Example usage
result = top_up_with_notifications(
budget_id='ci201910231405089666',
amount=10000.00,
currency='USD',
notification_email='finance@company.com'
)

Monthly Budget Replenishment

Automatically replenish budgets at the start of each month:

async function monthlyBudgetReplenishment(budgetConfigs) {
const results = {
timestamp: new Date().toISOString(),
month: new Date().toLocaleDateString('en-US', { month: 'long', year: 'numeric' }),
budgets: []
};

console.log(`\n=== Monthly Budget Replenishment ===`);
console.log(`Date: ${results.timestamp}`);
console.log(`Month: ${results.month}`);
console.log(`Processing ${budgetConfigs.length} budgets...\n`);

for (const config of budgetConfigs) {
const uniqueOrderId = `monthly_${Date.now()}_${config.budget_id.slice(-6)}`;

try {
console.log(`Replenishing: ${config.name}`);

// Top up to monthly allocation
const topUpResponse = await topUpBudgetAccount({
budget_id: config.budget_id,
top_up_amount: config.monthly_allocation.toFixed(2),
top_up_currency: config.currency,
unique_order_id: uniqueOrderId
});

if (topUpResponse.code === 'SUCCESS') {
console.log(` ✓ Initiated: ${config.currency} ${config.monthly_allocation}`);

// Wait for completion
await new Promise(resolve => setTimeout(resolve, 5000));

const statusResponse = await queryTopUpOrder(uniqueOrderId);
const status = statusResponse.data.status;

results.budgets.push({
budget_id: config.budget_id,
name: config.name,
amount: config.monthly_allocation,
currency: config.currency,
status: status,
unique_order_id: uniqueOrderId,
record_id: topUpResponse.data.record_id
});

const statusIcon = status === 'SUCCESS' ? '✓' : status === 'FAILED' ? '✗' : '⏳';
console.log(` ${statusIcon} Status: ${status}\n`);
} else {
console.log(` ✗ Failed to initiate\n`);
results.budgets.push({
budget_id: config.budget_id,
name: config.name,
status: 'FAILED_INITIATION',
error: topUpResponse
});
}

// Small delay between requests
await new Promise(resolve => setTimeout(resolve, 1000));

} catch (error) {
console.error(` ✗ Error: ${error.message}\n`);
results.budgets.push({
budget_id: config.budget_id,
name: config.name,
status: 'ERROR',
error: error.message
});
}
}

// Summary
const successful = results.budgets.filter(b => b.status === 'SUCCESS').length;
const failed = results.budgets.filter(b => b.status === 'FAILED' || b.status === 'FAILED_INITIATION').length;
const processing = results.budgets.filter(b => b.status === 'PROCESSING').length;

console.log(`\n=== Replenishment Summary ===`);
console.log(`Total: ${results.budgets.length}`);
console.log(`✓ Successful: ${successful}`);
console.log(`✗ Failed: ${failed}`);
console.log(`⏳ Processing: ${processing}`);

return results;
}

// Example usage with cron job (runs on 1st of each month at 00:00)
const budgetConfigs = [
{
budget_id: 'ci201910231405089666',
name: 'Marketing Department',
monthly_allocation: 50000.00,
currency: 'USD'
},
{
budget_id: 'ci201910231405089667',
name: 'Operations',
monthly_allocation: 75000.00,
currency: 'USD'
},
{
budget_id: 'ci201910231405089668',
name: 'Travel & Entertainment',
monthly_allocation: 25000.00,
currency: 'USD'
}
];

// Schedule for 1st of every month at midnight
// In production, use a proper scheduler like node-cron
const results = await monthlyBudgetReplenishment(budgetConfigs);

Best Practices

  • Generate Unique IDs: Use timestamps + UUIDs to ensure unique unique_order_id values
  • Always Verify Status: Don't assume top-up succeeded - always check status with Query Top-Up Order endpoint
  • Implement Polling: Poll status periodically (every 3-5 seconds) until SUCCESS or FAILED
  • Set Timeouts: Define maximum polling attempts to avoid infinite loops
  • Amount Validation: Validate amounts are positive and properly formatted before submission
  • Currency Matching: Ensure top-up currency matches budget account currency
  • Idempotency: Save unique_order_id to safely retry on network failures
  • Balance Monitoring: Monitor budget balances and set up low-balance alerts
  • Audit Logging: Log all top-up attempts and results for financial reconciliation

Top-Up Workflow

Standard Process

  1. Check Current Balance: Query budget balance to determine top-up needs
  2. Generate Unique ID: Create unique unique_order_id for this top-up
  3. Initiate Top-Up: Call this endpoint with budget details and amount
  4. Store Reference: Save record_id and unique_order_id for tracking
  5. Poll Status: Periodically query status using Query Top-Up Order endpoint
  6. Wait for Completion: Continue polling until status is SUCCESS or FAILED
  7. Verify Balance: Confirm budget balance increased by expected amount
  8. Update Records: Update your financial systems with completed top-up
  • Query Top-Up Order - Check the status of a top-up using unique_order_id
  • Query Budget Balance - View current budget balance before and after top-ups
  • Create Budget Account - Create budget accounts to receive top-ups
  • Query Budget Details - Get detailed information about budget accounts

Troubleshooting

Duplicate unique_order_id Error (400)

  • Cause: The unique_order_id has already been used for a previous top-up
  • Solution:
    • Generate a new unique ID for each top-up request
    • If retrying, query the existing order status first
    • Use timestamp + random component to ensure uniqueness

Invalid Amount Format (400)

  • Cause: Amount is not formatted correctly
  • Solution:
    • Provide amount as a string (e.g., "10.45" not 10.45)
    • Use maximum 2 decimal places
    • Ensure amount is positive

Budget Not Found (404)

  • Cause: The specified budget_id doesn't exist
  • Solution:
    • Verify the budget ID is correct
    • Ensure the budget account was created successfully
    • Check if budget was deleted

Top-Up Stuck in PROCESSING

  • Cause: Payment processing is delayed or failed
  • Solution:
    • Continue polling for up to 5-10 minutes
    • Verify your funding source is valid
    • Contact support if status doesn't update

Currency Mismatch

  • Cause: Top-up currency doesn't match budget currency
  • Solution:
    • Query budget details to confirm currency
    • Use matching currency for top-up
    • Contact support if currency conversion is needed

Security Considerations

  • Unique Order IDs: Treat as sensitive - they prevent duplicate charges
  • Amount Validation: Always validate amounts on your side before submission
  • Audit Trail: Maintain comprehensive logs of all top-up operations
  • Access Control: Restrict top-up permissions to authorized personnel only
  • Balance Monitoring: Set up alerts for unusual top-up patterns
  • Idempotency: Leverage idempotency to safely handle retries and network failures

Interactive API Explorer