Skip to main content

Creating Transfers to Bank Accounts

Transfers allow you to send funds from your Omise account balance to recipient bank accounts. This guide covers creating single and batch transfers, handling fees, and tracking transfer status.

Overviewโ€‹

Transfers in Omise enable you to:

  • Send Payouts: Transfer funds to recipients
  • Automated Processing: Immediate or scheduled transfers
  • Fee Management: Understand and handle transfer fees
  • Status Tracking: Monitor transfer progress in real-time
  • Batch Operations: Process multiple transfers efficiently
  • Failure Handling: Handle failed transfers gracefully

Key Featuresโ€‹

  • Instant Transfers: Funds sent immediately (subject to bank processing)
  • Fee Transparency: Clear fee structure per transfer
  • Multiple Recipients: Send to any created recipient
  • Metadata Support: Track custom information
  • Webhook Notifications: Real-time status updates
  • Balance Validation: Automatic balance checks

Transfer Requirementsโ€‹

Before creating a transfer:

  1. Sufficient Balance: Must have enough funds in your account
  2. Active Recipient: Recipient must be verified and active
  3. Minimum Amount: Must meet minimum transfer requirements
  4. Fee Coverage: Balance must cover transfer amount + fees

Transfer Feesโ€‹

const TRANSFER_FEES = {
thb: {
domestic: 2500, // 25 THB per transfer
minimum: 2000 // 20 THB minimum transfer
}
};

function calculateTransferCost(amount) {
return {
amount: amount,
fee: TRANSFER_FEES.thb.domestic,
total: amount + TRANSFER_FEES.thb.domestic
};
}

// Example
const cost = calculateTransferCost(100000); // 1,000 THB
console.log(`Amount: ${cost.amount / 100} THB`);
console.log(`Fee: ${cost.fee / 100} THB`);
console.log(`Total deducted: ${cost.total / 100} THB`);

Creating Transfers via APIโ€‹

Basic Transferโ€‹

const omise = require('omise')({
secretKey: 'skey_test_123456789',
});

// Create a transfer
async function createTransfer(recipientId, amount) {
try {
// Check balance first
const balance = await omise.balance.retrieve();
const totalCost = amount + 2500; // amount + fee

if (balance.available < totalCost) {
throw new Error(`Insufficient balance. Need ${totalCost}, have ${balance.available}`);
}

// Create transfer
const transfer = await omise.transfers.create({
recipient: recipientId,
amount: amount,
metadata: {
purpose: 'vendor_payment',
invoice_id: 'INV-001',
payment_date: new Date().toISOString()
}
});

console.log('Transfer created:', transfer.id);
console.log('Amount:', transfer.amount / 100, 'THB');
console.log('Fee:', transfer.fee / 100, 'THB');
console.log('Status:', transfer.status);

return transfer;
} catch (error) {
console.error('Transfer failed:', error.message);
throw error;
}
}

// Create transfer with full validation
async function createValidatedTransfer(recipientId, amount, metadata = {}) {
// Validate recipient
const recipient = await omise.recipients.retrieve(recipientId);

if (!recipient.active) {
throw new Error('Recipient is not active');
}

if (!recipient.verified) {
throw new Error('Recipient is not verified');
}

// Validate amount
if (amount < 2000) { // 20 THB minimum
throw new Error('Amount below minimum transfer amount');
}

// Check balance
const balance = await omise.balance.retrieve();
const totalCost = amount + 2500;

if (balance.available < totalCost) {
return {
success: false,
reason: 'insufficient_balance',
available: balance.available,
required: totalCost,
shortfall: totalCost - balance.available
};
}

// Create transfer
const transfer = await omise.transfers.create({
recipient: recipientId,
amount: amount,
metadata: metadata
});

return {
success: true,
transfer: transfer,
recipient_name: recipient.name,
bank: recipient.bank_account.brand
};
}

// Example usage
createTransfer('recp_test_123456789', 100000); // 1,000 THB

createValidatedTransfer('recp_test_123456789', 500000, {
payment_type: 'monthly_salary',
employee_id: 'EMP-042',
period: '2024-01'
}).then(result => {
if (result.success) {
console.log(`โœ“ Transfer ${result.transfer.id} created`);
console.log(` To: ${result.recipient_name}`);
console.log(` Bank: ${result.bank}`);
} else {
console.log(`โœ— Transfer failed: ${result.reason}`);
}
});

API Responseโ€‹

{
"object": "transfer",
"id": "trsf_test_5xyz789abc",
"livemode": false,
"location": "/transfers/trsf_test_5xyz789abc",
"recipient": "recp_test_123456789",
"bank_account": {
"object": "bank_account",
"brand": "bbl",
"last_digits": "7890",
"name": "John Doe"
},
"sent": true,
"paid": false,
"amount": 100000,
"currency": "thb",
"fee": 2500,
"failure_code": null,
"failure_message": null,
"transaction": "trxn_test_5xyz789abc",
"created": "2024-01-15T10:30:00Z",
"metadata": {
"purpose": "vendor_payment",
"invoice_id": "INV-001"
}
}

Transfer Status Trackingโ€‹

Status Lifecycleโ€‹

const TRANSFER_STATUSES = {
'pending': 'Transfer created, waiting to be sent',
'sent': 'Transfer sent to bank, waiting for confirmation',
'paid': 'Transfer completed successfully',
'failed': 'Transfer failed',
'reversed': 'Transfer was reversed'
};

async function trackTransferStatus(transferId) {
const transfer = await omise.transfers.retrieve(transferId);

console.log(`Transfer ${transferId}:`);
console.log(` Status: ${transfer.sent ? 'Sent' : 'Pending'}`);
console.log(` Paid: ${transfer.paid ? 'Yes' : 'No'}`);

if (transfer.failure_code) {
console.log(` Failed: ${transfer.failure_message}`);
}

return {
id: transfer.id,
sent: transfer.sent,
paid: transfer.paid,
failed: !!transfer.failure_code,
status_description: getStatusDescription(transfer)
};
}

function getStatusDescription(transfer) {
if (transfer.failure_code) return 'Failed';
if (transfer.paid) return 'Completed';
if (transfer.sent) return 'Processing';
return 'Pending';
}

Common Use Casesโ€‹

1. Payroll Processingโ€‹

class PayrollProcessor:
def __init__(self):
self.transfer_fee = 2500

def process_payroll(self, employees):
"""Process payroll for multiple employees"""
results = {
'processed': [],
'failed': [],
'total_amount': 0,
'total_fees': 0
}

# Check total required balance
total_needed = sum(emp['salary'] for emp in employees)
total_fees = len(employees) * self.transfer_fee
grand_total = total_needed + total_fees

balance = omise.Balance.retrieve()
if balance.available < grand_total:
raise ValueError(f"Insufficient balance for payroll. Need {grand_total}, have {balance.available}")

# Process each employee
for employee in employees:
try:
transfer = omise.Transfer.create(
recipient=employee['recipient_id'],
amount=employee['salary'],
metadata={
'type': 'payroll',
'employee_id': employee['employee_id'],
'employee_name': employee['name'],
'period': employee['pay_period'],
'department': employee['department']
}
)

results['processed'].append({
'employee_id': employee['employee_id'],
'name': employee['name'],
'transfer_id': transfer.id,
'amount': employee['salary'],
'fee': self.transfer_fee
})

results['total_amount'] += employee['salary']
results['total_fees'] += self.transfer_fee

print(f"โœ“ Processed: {employee['name']} - {employee['salary']/100:.2f} THB")

except Exception as e:
results['failed'].append({
'employee_id': employee['employee_id'],
'name': employee['name'],
'error': str(e)
})
print(f"โœ— Failed: {employee['name']} - {str(e)}")

# Generate report
print(f"\n{'='*50}")
print(f"Payroll Summary:")
print(f" Processed: {len(results['processed'])}")
print(f" Failed: {len(results['failed'])}")
print(f" Total Amount: {results['total_amount']/100:.2f} THB")
print(f" Total Fees: {results['total_fees']/100:.2f} THB")
print(f" Grand Total: {(results['total_amount'] + results['total_fees'])/100:.2f} THB")

return results

# Example usage
processor = PayrollProcessor()
employees = [
{
'employee_id': 'EMP-001',
'name': 'John Doe',
'recipient_id': 'recp_test_111',
'salary': 5000000, # 50,000 THB
'department': 'Engineering',
'pay_period': '2024-01'
},
{
'employee_id': 'EMP-002',
'name': 'Jane Smith',
'recipient_id': 'recp_test_222',
'salary': 4500000, # 45,000 THB
'department': 'Marketing',
'pay_period': '2024-01'
}
]

payroll_results = processor.process_payroll(employees)

2. Vendor Payment Managementโ€‹

class VendorPaymentManager
def initialize
@transfer_fee = 2500
end

def pay_invoice(invoice)
# Validate invoice
raise 'Invoice already paid' if invoice[:paid]
raise 'Invoice not approved' unless invoice[:approved]

# Get recipient
recipient_id = get_recipient_for_vendor(invoice[:vendor_id])

# Create transfer
transfer = Omise::Transfer.create(
recipient: recipient_id,
amount: invoice[:amount],
metadata: {
type: 'invoice_payment',
invoice_id: invoice[:invoice_id],
vendor_id: invoice[:vendor_id],
due_date: invoice[:due_date],
payment_terms: invoice[:payment_terms]
}
)

# Update invoice status
mark_invoice_paid(invoice[:invoice_id], transfer.id)

puts "โœ“ Invoice #{invoice[:invoice_id]} paid"
puts " Vendor: #{invoice[:vendor_name]}"
puts " Amount: #{invoice[:amount] / 100.0} THB"
puts " Transfer: #{transfer.id}"

transfer
end

def batch_pay_due_invoices(date = Date.today)
# Get invoices due today or earlier
due_invoices = get_due_invoices(date)

results = {
successful: [],
failed: []
}

due_invoices.each do |invoice|
begin
transfer = pay_invoice(invoice)
results[:successful] << {
invoice_id: invoice[:invoice_id],
transfer_id: transfer.id,
amount: invoice[:amount]
}
rescue => e
results[:failed] << {
invoice_id: invoice[:invoice_id],
error: e.message
}
end
end

generate_payment_report(results)
results
end

private

def get_recipient_for_vendor(vendor_id)
# Implement vendor-to-recipient mapping
"recp_test_#{vendor_id}"
end

def mark_invoice_paid(invoice_id, transfer_id)
# Implement invoice update logic
end

def get_due_invoices(date)
# Implement invoice retrieval logic
[]
end

def generate_payment_report(results)
puts "\n#{'='*50}"
puts "Vendor Payment Report"
puts "Successful: #{results[:successful].length}"
puts "Failed: #{results[:failed].length}"

total = results[:successful].sum { |r| r[:amount] }
puts "Total Paid: #{total / 100.0} THB"
end
end

3. Marketplace Seller Payoutsโ€‹

class MarketplacePayoutManager {
private $transferFee = 2500;
private $platformCommission = 0.15; // 15%

public function calculateSellerPayout($orderId) {
$order = $this->getOrder($orderId);

$grossAmount = $order['total_amount'];
$commission = intval($grossAmount * $this->platformCommission);
$netAmount = $grossAmount - $commission;

return [
'order_id' => $orderId,
'gross_amount' => $grossAmount,
'commission' => $commission,
'net_amount' => $netAmount,
'transfer_fee' => $this->transferFee,
'seller_receives' => $netAmount
];
}

public function processSellerPayout($sellerId, $orders) {
// Calculate total payout
$totalPayout = 0;
$orderDetails = [];

foreach ($orders as $orderId) {
$payout = $this->calculateSellerPayout($orderId);
$totalPayout += $payout['net_amount'];
$orderDetails[] = $payout;
}

// Get seller recipient
$recipientId = $this->getSellerRecipient($sellerId);

// Create transfer
try {
$transfer = OmiseTransfer::create([
'recipient' => $recipientId,
'amount' => $totalPayout,
'metadata' => [
'type' => 'seller_payout',
'seller_id' => $sellerId,
'order_count' => count($orders),
'order_ids' => implode(',', $orders),
'payout_period' => date('Y-m')
]
]);

// Mark orders as paid
foreach ($orders as $orderId) {
$this->markOrderPaid($orderId, $transfer['id']);
}

echo "โœ“ Payout processed for seller {$sellerId}\n";
echo " Orders: " . count($orders) . "\n";
echo " Amount: " . ($totalPayout / 100) . " THB\n";
echo " Transfer: {$transfer['id']}\n";

return [
'success' => true,
'transfer' => $transfer,
'orders' => $orderDetails,
'total_amount' => $totalPayout
];

} catch (Exception $e) {
echo "โœ— Payout failed: {$e->getMessage()}\n";
return [
'success' => false,
'error' => $e->getMessage()
];
}
}

public function scheduledPayoutRun() {
// Get all sellers eligible for payout
$sellers = $this->getSellersForPayout();

$results = [
'successful' => 0,
'failed' => 0,
'total_amount' => 0
];

foreach ($sellers as $seller) {
$unpaidOrders = $this->getUnpaidOrders($seller['id']);

if (empty($unpaidOrders)) {
continue;
}

$result = $this->processSellerPayout($seller['id'], $unpaidOrders);

if ($result['success']) {
$results['successful']++;
$results['total_amount'] += $result['total_amount'];
} else {
$results['failed']++;
}
}

echo "\n" . str_repeat('=', 50) . "\n";
echo "Marketplace Payout Run Complete\n";
echo "Successful: {$results['successful']}\n";
echo "Failed: {$results['failed']}\n";
echo "Total Paid: " . ($results['total_amount'] / 100) . " THB\n";

return $results;
}

private function getOrder($orderId) {
// Implement order retrieval
return ['order_id' => $orderId, 'total_amount' => 100000];
}

private function getSellerRecipient($sellerId) {
// Implement seller-to-recipient mapping
return "recp_test_{$sellerId}";
}

private function markOrderPaid($orderId, $transferId) {
// Implement order update
}

private function getSellersForPayout() {
// Implement seller retrieval
return [];
}

private function getUnpaidOrders($sellerId) {
// Implement unpaid order retrieval
return [];
}
}

Best Practicesโ€‹

1. Always Validate Before Transferโ€‹

async function safeTransfer(recipientId, amount, metadata) {
// Pre-transfer validation checklist
const validations = [];

// 1. Validate recipient
try {
const recipient = await omise.recipients.retrieve(recipientId);
if (!recipient.active) validations.push('Recipient not active');
if (!recipient.verified) validations.push('Recipient not verified');
} catch (error) {
validations.push('Recipient not found');
}

// 2. Validate amount
if (amount < 2000) validations.push('Amount below minimum');
if (amount % 1 !== 0) validations.push('Amount must be integer');

// 3. Check balance
const balance = await omise.balance.retrieve();
const totalCost = amount + 2500;
if (balance.available < totalCost) {
validations.push(`Insufficient balance: need ${totalCost}, have ${balance.available}`);
}

// 4. Validate metadata
if (metadata && typeof metadata !== 'object') {
validations.push('Metadata must be an object');
}

if (validations.length > 0) {
throw new Error(`Validation failed:\n- ${validations.join('\n- ')}`);
}

// All validations passed, create transfer
return await omise.transfers.create({
recipient: recipientId,
amount: amount,
metadata: metadata
});
}

2. Implement Retry Logicโ€‹

def create_transfer_with_retry(recipient_id, amount, max_retries=3, metadata=None):
"""Create transfer with retry logic for transient failures"""

for attempt in range(max_retries):
try:
transfer = omise.Transfer.create(
recipient=recipient_id,
amount=amount,
metadata=metadata or {}
)
return transfer

except omise.errors.InvalidRequestError as e:
# Don't retry validation errors
raise

except omise.errors.APIError as e:
if attempt < max_retries - 1:
wait_time = 2 ** attempt # Exponential backoff
print(f"Attempt {attempt + 1} failed, retrying in {wait_time}s...")
time.sleep(wait_time)
else:
raise

return None

3. Monitor and Log Transfersโ€‹

class TransferLogger
def self.log_transfer(transfer, context = {})
log_entry = {
event: 'transfer_created',
transfer_id: transfer.id,
recipient: transfer.recipient,
amount: transfer.amount,
fee: transfer.fee,
status: transfer.sent ? 'sent' : 'pending',
created_by: context[:user_id],
timestamp: Time.now.iso8601,
metadata: transfer.metadata
}

# Save to log file
File.open('transfers.log', 'a') do |f|
f.puts JSON.generate(log_entry)
end

# Also save to database for reporting
save_to_database(log_entry)
end

def self.save_to_database(entry)
# Implement database logging
end
end

FAQโ€‹

How long do transfers take to complete?โ€‹

Transfers are typically completed within 1-2 business days. The exact timing depends on the recipient's bank and when the transfer was initiated. Transfers created on weekends or holidays may take longer.

What happens if a transfer fails?โ€‹

If a transfer fails, you'll see a failure_code and failure_message on the transfer object. Common reasons include invalid account details or recipient account issues. The amount is automatically returned to your balance.

Can I cancel a transfer after it's created?โ€‹

No, transfers cannot be canceled once created. If you need to recover funds, you would need to request the recipient to send the money back.

Are transfer fees refunded if a transfer fails?โ€‹

Yes, if a transfer fails, both the transfer amount and fee are returned to your available balance.

Can I transfer to international bank accounts?โ€‹

Currently, Omise supports transfers to Thai bank accounts only. For international payments, you would need to use alternative methods.

What's the maximum transfer amount?โ€‹

There's no maximum transfer amount set by Omise, but your account balance and individual bank limits may apply. Very large transfers may require additional verification.

How do I know when a transfer is completed?โ€‹

You can track transfer status via the API or set up webhooks to receive notifications when transfers are sent (transfer.send) or completed (transfer.pay).

Can I schedule transfers for future dates?โ€‹

Yes, you can use Transfer Schedules to automate recurring transfers or schedule them for specific dates.

Next Stepsโ€‹