The Virtual Bank Accounts API allows you to create and manage fiat accounts for your business or customers.
Each virtual account behaves like a regular bank account — it can receive deposits, display balances, and participate in transfers — all through Tsara's infrastructure.
Currently, virtual accounts are supported for NGN (Nigerian Naira).
Overview
- Create new virtual bank accounts (dynamic or reserved)
- Retrieve account details and balances
- List transactions with filtering
- Choose settlement options (Instant or T+1)
- Get webhook notifications on deposits or activity
- Support both temporary and permanent accounts
Key Features
| Feature | Description |
|---|---|
| Instant Account Creation | Generate accounts in seconds via API |
| Real Nigerian Banks | Powered by SafeHaven MFB, GTBank, and other partner banks |
| Automatic Settlement | Funds automatically settle to your Tsara balance |
| Webhook Notifications | Real-time alerts for all transactions |
| Multiple Account Types | Dynamic (temporary) and Reserved (permanent) |
Virtual Account Types
Understanding the difference between account types:
| Feature | Dynamic Virtual Account | Reserved Virtual Account |
|---|---|---|
| Lifespan | Temporary (expires after time limit or first payment) | Permanent (never expires) |
| Use Case | Single payment, invoices, one-time checkouts | Customer wallets, recurring payments, long-term use |
| Creation | No customer identity required | Requires verified customer identity |
| Reusability | Single-use or time-limited | Unlimited reuse |
| Account Name | Generic business name | Customer's actual name (from identity verification) |
| Best For | E-commerce checkouts, invoices | Subscription services, user wallets, savings |
Create a Dynamic Virtual Account
Generate a temporary virtual account for one-time or time-limited payments.
Endpoint
POST /fiat/virtual-accounts
Headers
| Header | Value | Required |
|---|---|---|
Authorization | Bearer YOUR_SECRET_KEY | Yes |
Content-Type | application/json | Yes |
Request Parameters
| Parameter | Type | Required | Description | Example |
|---|---|---|---|---|
currency | string | Yes | Currency code. Must be "NGN" | "NGN" |
amount | number | No | Expected payment amount in kobo (for validation) | 500000 (₦5,000.00) |
validFor | number | No | Account validity in seconds (default: 86400 = 24 hours, max: 2592000 = 30 days) | 1800 (30 minutes) |
business_id | string | No | Your internal business/order reference (max 100 chars) | "order_12345" |
settlement_option | string | No | Settlement speed: "instant" or "T+1" (default: "instant") | "instant" |
metadata | object | No | Custom tracking data (max 10 keys, 500 chars per value) | {"order_id": "ORD-123"} |
Settlement Options Explained
| Option | Description | Settlement Time | Fees | Best For |
|---|---|---|---|---|
instant | Funds settle immediately to your balance | Real-time | Standard | Immediate fulfillment needs |
T+1 | Funds settle next business day | 1 business day | Lower fees | High-volume, non-urgent |
default | Uses your account default setting | Varies | As configured | General use |
Important Notes
- Amount validation: If provided, account will only accept the exact amount specified
- Expiration: Accounts auto-expire after
validForseconds or after first successful payment (whichever comes first) - Account name: Will show your business name (configured in dashboard)
- Reusability: Dynamic accounts are typically single-use
Request Example
{
"currency": "NGN",
"amount": 500000,
"validFor": 1800,
"business_id": "order_12345",
"settlement_option": "instant",
"metadata": {
"order_id": "ORD-123",
"customer_email": "[email protected]",
"product": "Premium Plan"
}
}curl -X POST "https://sandbox.tsara.ng/v1/fiat/virtual-accounts" \
-H "Authorization: Bearer YOUR_SECRET_KEY" \
-H "Content-Type: application/json" \
-d '{
"currency": "NGN",
"amount": 500000,
"validFor": 1800,
"business_id": "order_12345",
"settlement_option": "instant",
"metadata": {
"order_id": "ORD-123"
}
}'Response
{
"success": true,
"status": "success",
"status_code": 200,
"message": "Virtual account created successfully",
"data": {
"id": "bank_695fc9b9d4e992",
"uid": "bank_695fc9b9d4e992",
"reference": "ref_1738318449",
"bank_name": "SafeHaven MFB",
"bank_code": "090286",
"account_number": "6028678511",
"account_name": "Acme Ltd",
"currency": "NGN",
"type": "dynamic",
"expected_amount": 500000,
"settlement_option": "instant",
"status": "active",
"expires_at": "2025-01-31T12:30:00Z",
"metadata": {
"order_id": "ORD-123",
"customer_email": "[email protected]"
},
"created_at": "2025-01-31T12:00:00Z",
"updated_at": "2025-01-31T12:00:00Z"
},
"request_id": "req_1738318449"
}Response Fields
| Field | Type | Description |
|---|---|---|
success | boolean | Request success status |
status | string | Request status text |
status_code | number | HTTP status code |
message | string | Human-readable message |
data.id | string | Virtual account ID |
data.reference | string | Unique account reference |
data.bank_name | string | Nigerian bank name |
data.bank_code | string | Nigerian bank code (NIBSS code) |
data.account_number | string | 10-digit Nigerian account number |
data.account_name | string | Account name shown to payers |
data.currency | string | Currency code (NGN) |
data.type | string | Account type: dynamic or reserved |
data.expected_amount | number | Expected payment amount (null if not specified) |
data.settlement_option | string | Settlement speed |
data.status | string | Account status: active, expired, closed |
data.expires_at | string | Expiration timestamp (ISO 8601) |
data.metadata | object | Custom metadata |
data.created_at | string | Creation timestamp |
data.updated_at | string | Last update timestamp |
💡 The account_number and bank_name can be shared with customers to receive NGN deposits.
Account Status Values
| Status | Description |
|---|---|
active | Account is operational and can receive deposits |
expired | Account has passed expiration time |
closed | Account manually closed or received payment (for single-use) |
suspended | Account temporarily disabled (contact support) |
Error Responses
{
"success": false,
"status_code": 400,
"error": {
"code": "validation_error",
"message": "Invalid request parameters",
"details": [
{
"field": "amount",
"message": "Amount must be greater than 100 (₦1.00)"
}
]
}
}Common Errors
| Status Code | Error Code | Description |
|---|---|---|
| 400 | validation_error | Invalid request parameters |
| 400 | invalid_currency | Currency not supported (use NGN) |
| 400 | invalid_validity_period | validFor exceeds maximum (30 days) |
| 401 | unauthorized | Invalid or missing API key |
| 429 | rate_limit_exceeded | Too many accounts created |
Create a Reserved Virtual Account
Generate a permanent virtual account tied to a verified customer identity.
Endpoint
POST /fiat/reserved-accounts
Headers
| Header | Value | Required |
|---|---|---|
Authorization | Bearer YOUR_SECRET_KEY | Yes |
Content-Type | application/json | Yes |
Request Parameters
| Parameter | Type | Required | Description | Example |
|---|---|---|---|---|
currency | string | Yes | Currency code. Must be "NGN" | "NGN" |
identity_id | string | Yes | ID of verified customer identity (from Identity Verification API) | "identity_abc123" |
phone_number | string | Yes | Customer phone number (11 digits, starts with 0) | "08012345678" |
email | string | Yes | Customer email address | "[email protected]" |
reference | string | No | Your unique reference for this account (max 100 chars) | "user_123_account" |
metadata | object | No | Custom tracking data | {"user_id": "usr_123"} |
⚠️ Important: Identity ID Requirement
Reserved accounts require a verified customer identity because:
- Account name shows customer's actual name (from BVN/NIN verification)
- Ensures KYC compliance for permanent accounts
- Prevents account misuse and fraud
How to get identity_id:
- Create customer:
POST /customers - Verify identity:
POST /customers/identity/initiate(BVN or NIN) - Validate verification:
POST /customers/identity/validate(with OTP) - Use returned
identity_idto create reserved account
See Identity Verification for details.
Request Example
{
"currency": "NGN",
"identity_id": "identity_abc123",
"phone_number": "08012345678",
"email": "[email protected]",
"reference": "user_123_main_account",
"metadata": {
"user_id": "usr_123",
"account_type": "savings"
}
}curl -X POST "https://sandbox.tsara.ng/v1/fiat/reserved-accounts" \
-H "Authorization: Bearer YOUR_SECRET_KEY" \
-H "Content-Type: application/json" \
-d '{
"currency": "NGN",
"identity_id": "identity_abc123",
"phone_number": "08012345678",
"email": "[email protected]",
"reference": "user_123_main_account",
"metadata": {
"user_id": "usr_123"
}
}'Response
{
"success": true,
"status": "success",
"status_code": 200,
"message": "Reserved account created successfully",
"data": {
"id": "bank_reserved_123",
"reference": "user_123_main_account",
"bank_name": "GTBank",
"bank_code": "058",
"account_number": "0123456789",
"account_name": "John Doe",
"currency": "NGN",
"type": "reserved",
"customer_id": "cus_123",
"identity_id": "identity_abc123",
"status": "active",
"expires_at": null,
"metadata": {
"user_id": "usr_123",
"account_type": "savings"
},
"created_at": "2025-01-31T12:00:00Z",
"updated_at": "2025-01-31T12:00:00Z"
}
}Reserved Account Features
- Permanent: Never expires (unless manually closed)
- Personal name: Shows customer's real name from identity verification
- Unlimited use: Can receive unlimited deposits
- Consistent: Same account number for all transactions
- Traceable: Linked to verified customer identity
Retrieve a Virtual Account
Get account details by ID or reference.
Endpoint
GET /fiat/virtual-accounts?id={account_id}
or
GET /fiat/virtual-accounts?reference={account_reference}
Query Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
id | string | No* | Virtual account ID |
reference | string | No* | Your account reference |
*At least one parameter required
Example Request
curl -X GET "https://sandbox.tsara.ng/v1/fiat/virtual-accounts?id=bank_695fc9b9d4e992" \
-H "Authorization: Bearer YOUR_SECRET_KEY"Response
{
"success": true,
"status": "success",
"status_code": 200,
"message": "Virtual account retrieved",
"data": {
"id": "bank_695fc9b9d4e992",
"reference": "ref_1738318449",
"bank_name": "SafeHaven MFB",
"bank_code": "090286",
"account_number": "6028678511",
"account_name": "Acme Ltd",
"currency": "NGN",
"type": "dynamic",
"balance": 0,
"total_received": 0,
"payment_count": 0,
"settlement_option": "instant",
"status": "active",
"expires_at": "2025-01-31T12:30:00Z",
"created_at": "2025-01-31T12:00:00Z",
"last_payment_at": null
}
}Get Account Balance
Retrieve current balance for a virtual account.
Endpoint
GET /fiat/virtual-accounts/balance?id={account_id}
Query Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
id | string | Yes | Virtual account ID |
Example Request
curl -X GET "https://sandbox.tsara.ng/v1/fiat/virtual-accounts/balance?id=bank_695fc9b9d4e992" \
-H "Authorization: Bearer YOUR_SECRET_KEY"Response
{
"success": true,
"status": "success",
"status_code": 200,
"message": "Account balance retrieved",
"data": {
"account_id": "bank_695fc9b9d4e992",
"available_balance": 250000,
"pending_balance": 50000,
"total_balance": 300000,
"currency": "NGN",
"last_settlement_at": "2025-01-31T10:00:00Z",
"updated_at": "2025-01-31T12:10:00Z"
}
}Balance Fields
| Field | Description |
|---|---|
available_balance | Settled funds available for withdrawal |
pending_balance | Funds pending settlement (T+1 accounts) |
total_balance | Sum of available + pending |
List Transactions
Retrieve transaction history for virtual accounts.
Endpoint
GET /fiat/virtual-accounts/transactions?page={page}&limit={limit}
Query Parameters
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
page | number | No | 1 | Page number |
limit | number | No | 20 | Items per page (max: 100) |
account_id | string | No | - | Filter by account ID |
type | string | No | - | Filter by type: incoming, outgoing |
status | string | No | - | Filter by status: success, pending, failed |
from_date | string | No | - | Start date (ISO 8601) |
to_date | string | No | - | End date (ISO 8601) |
Example Request
curl -X GET "https://sandbox.tsara.ng/v1/fiat/virtual-accounts/transactions?account_id=bank_695fc9b9d4e992&page=1&limit=20" \
-H "Authorization: Bearer YOUR_SECRET_KEY"Response
{
"success": true,
"status": "success",
"status_code": 200,
"message": "Transactions retrieved",
"data": [
{
"id": "txn_001",
"account_id": "bank_695fc9b9d4e992",
"type": "incoming",
"amount": 500000,
"currency": "NGN",
"description": "Customer payment",
"status": "success",
"reference": "order_001",
"sender_bank": "Access Bank",
"sender_account": "0987654321",
"sender_name": "Jane Smith",
"balance_before": 0,
"balance_after": 500000,
"settled_at": "2025-01-31T12:00:30Z",
"created_at": "2025-01-31T12:00:00Z"
},
{
"id": "txn_002",
"account_id": "bank_695fc9b9d4e992",
"type": "outgoing",
"amount": 200000,
"currency": "NGN",
"description": "Vendor payout",
"status": "success",
"reference": "payout_001",
"recipient_bank": "GTBank",
"recipient_account": "0123456789",
"recipient_name": "Vendor Co",
"balance_before": 500000,
"balance_after": 300000,
"created_at": "2025-01-31T12:05:00Z"
}
],
"pagination": {
"page": 1,
"limit": 20,
"total": 2,
"total_pages": 1,
"has_more": false
},
"request_id": "req_xyz"
}Transaction Fields
| Field | Type | Description |
|---|---|---|
type | string | Transaction direction: incoming or outgoing |
sender_bank | string | Bank of sender (for incoming) |
sender_account | string | Account number of sender |
sender_name | string | Name of sender |
balance_before | number | Account balance before transaction |
balance_after | number | Account balance after transaction |
settled_at | string | Settlement timestamp |
Webhook Notifications
Real-time notifications for all virtual account activity.
Event Types
| Event | Description | Trigger |
|---|---|---|
fiat.received | Deposit received | When customer sends money to virtual account |
fiat.sent | Payout completed | When transfer from account succeeds |
fiat.failed | Transaction failed | When deposit or payout fails |
account.expired | Account expired | When dynamic account passes expiration time |
Example Webhook Payload: fiat.received
{
"id": "evt_100",
"type": "fiat.received",
"created_at": "2025-01-31T12:00:00Z",
"data": {
"transaction_id": "txn_001",
"account_id": "bank_695fc9b9d4e992",
"account_number": "6028678511",
"amount": 500000,
"currency": "NGN",
"reference": "order_001",
"sender": {
"bank": "Access Bank",
"account_number": "0987654321",
"account_name": "Jane Smith"
},
"balance_before": 0,
"balance_after": 500000,
"settlement_option": "instant",
"status": "success",
"metadata": {
"order_id": "ORD-123"
},
"settled_at": "2025-01-31T12:00:30Z",
"created_at": "2025-01-31T12:00:00Z"
}
}Webhook Handler Example
app.post('/webhooks/tsara', (req, res) => {
const event = req.body;
if (event.type === 'fiat.received') {
const payment = event.data;
const orderId = payment.metadata.order_id;
await fulfillOrder(orderId);
await db.payments.create({
order_id: orderId,
amount: payment.amount,
transaction_id: payment.transaction_id,
sender: payment.sender.account_name,
status: 'completed'
});
await notifyCustomer(orderId, 'Payment received');
}
res.sendStatus(200);
});See Webhooks Security for signature verification.
Use Cases & Examples
E-commerce Checkout
Create dynamic account for order payment.
async function createOrderPaymentAccount(orderId, orderAmount) {
const response = await fetch('https://sandbox.tsara.ng/v1/fiat/virtual-accounts', {
method: 'POST',
headers: {
'Authorization': `Bearer ${SECRET_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
currency: 'NGN',
amount: orderAmount,
validFor: 3600,
business_id: `order_${orderId}`,
settlement_option: 'instant',
metadata: {
order_id: orderId,
customer_email: order.customer_email
}
})
});
const data = await response.json();
const account = data.data;
await db.orders.update(orderId, {
payment_account_number: account.account_number,
payment_bank: account.bank_name,
payment_expires_at: account.expires_at
});
return account;
}User Wallet System
Create reserved account for each user.
async function createUserWallet(userId) {
const user = await db.users.findById(userId);
const identity = await verifyUserIdentity(user.bvn);
const response = await fetch('https://sandbox.tsara.ng/v1/fiat/reserved-accounts', {
method: 'POST',
headers: {
'Authorization': `Bearer ${SECRET_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
currency: 'NGN',
identity_id: identity.id,
phone_number: user.phone,
email: user.email,
reference: `user_$USERID_wallet`,
metadata: {
user_id: userId,
account_type: 'wallet'
}
})
});
const data = await response.json();
await db.users.update(userId, {
wallet_account_id: data.data.id,
wallet_account_number: data.data.account_number,
wallet_bank_name: data.data.bank_name
});
return data.data;
}Invoice Payment
Generate account for invoice with expiration.
async function generateInvoicePaymentAccount(invoiceId, amount, dueDate) {
const validFor = Math.floor((new Date(dueDate) - Date.now()) / 1000);
const response = await fetch('https://sandbox.tsara.ng/v1/fiat/virtual-accounts', {
method: 'POST',
headers: {
'Authorization': `Bearer ${SECRET_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
currency: 'NGN',
amount: amount,
validFor: validFor,
business_id: `invoice_${invoiceId}`,
metadata: {
invoice_id: invoiceId,
due_date: dueDate
}
})
});
return response.json();
}Tips & Best Practices
-
Use amount validation for exact payments
{ amount: expectedAmount, validFor: 1800 }Account will only accept the exact amount.
-
Set appropriate expiration times
validFor: { 'checkout': 1800, // 30 minutes 'invoice': 86400 * 7, // 7 days 'quote': 3600 // 1 hour } -
Store account details
await db.virtual_accounts.create({ account_id: account.id, account_number: account.account_number, bank_name: account.bank_name, order_id: orderId, expires_at: account.expires_at }); -
Handle duplicate deposits
await db.payments.findOrCreate({ where: { transaction_id: payment.transaction_id }, defaults: { order_id: orderId, amount: payment.amount } }); -
Monitor expiration
cron.schedule('*/5 * * * *', async () => { const expiring = await db.virtual_accounts.findAll({ where: { expires_at: { $lte: Date.now() + 300000 }, status: 'pending' } }); for (const account of expiring) { await notifyExpiringAccount(account); } }); -
Choose settlement based on urgency
settlement_option: isUrgent ? 'instant' : 'T+1' -
Use reserved accounts for repeat customers
if (customer.repeat_customer) { return customer.reserved_account; } return await createDynamicAccount(order);
Troubleshooting
Account not receiving deposits
Cause: Account expired or customer using wrong details.
Solution:
- Check account status and expiration
- Verify account number and bank name shared correctly
- Ensure customer sending from Nigerian bank
- Check transaction limits (minimum ₦100)
Webhook not received
Cause: Webhook URL not configured or validation failing.
Solution:
- Configure webhook URL in dashboard
- Verify HMAC-SHA512 signature
- Ensure endpoint returns 200 OK within 10 seconds
- Check webhook logs in dashboard
Reserved account creation fails
Cause: Missing or invalid identity_id.
Solution:
// First verify identity
const identity = await verifyBVN(bvnNumber, otp);
// Then create account
const account = await createReservedAccount({
identity_id: identity.id,
phone_number: phone,
email: email
});Amount mismatch error
Cause: Customer sent different amount than expected.
Solution:
- For strict validation, set
amountparameter - For flexible amounts, omit
amountparameter - Handle partial payments in webhook
Account expired before payment
Cause: validFor too short.
Solution:
const validFor = Math.max(1800, estimatedPaymentTime * 2);Related Pages
- Bank Transfers — Move funds between accounts or to external banks
- Webhooks — Handle fiat transaction events securely
- Customers — Create customers linked to bank accounts
- Identity Verification — Verify customer identity for reserved accounts
- Errors & Status Codes — Complete error reference