Wallet
Wallet
The wallet system provides a pre-paid balance that your app can debit in real time. Merchants top up their wallet, and your app deducts from it as services are consumed -- no merchant approval needed for each individual transaction.
How It Works
- Your app creates a wallet top-up charge.
- The merchant approves and pays via the standard EPS payment flow.
- The payment amount is credited to the app-specific wallet for that store.
- Your app debits the wallet for each unit of service consumed (e.g., each SMS sent).
Each app has a separate wallet per store. Wallet balances from one app do not carry over to another.
Top Up the Wallet
POST /api/apps/v1/billing/wallet-topup
Authorization: Bearer sx_at_...
Content-Type: application/json
{
"name": "Wallet Top-up",
"amount": 500.00,
"currency": "BDT",
"return_url": "https://app.example.com/wallet/success"
}Parameters:
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Display name shown to the merchant |
amount | number | Yes | Top-up amount in BDT (min 10.00, max 50,000.00) |
currency | string | Yes | Must be "BDT" |
return_url | string | No | URL to redirect after payment |
The approval and payment flow is identical to one-time charges. After successful payment, the amount is automatically credited to the merchant's wallet for your app.
Check Balance
GET /api/apps/v1/billing/wallet
Authorization: Bearer sx_at_...Response:
{
"wallet_id": 3,
"balance": 350.00,
"currency": "BDT",
"total_topup": 500.00,
"total_spent": 150.00
}| Field | Description |
|---|---|
wallet_id | Unique identifier for this wallet |
balance | Current available balance |
currency | Always "BDT" |
total_topup | Lifetime total of all top-ups |
total_spent | Lifetime total of all deductions |
Debit the Wallet
Deduct an amount from the wallet when a service is consumed:
POST /api/apps/v1/billing/wallet/debit
Authorization: Bearer sx_at_...
Content-Type: application/json
{
"amount": 2.50,
"description": "SMS sent to +8801712345678",
"metadata": { "sms_id": "msg-123" }
}Parameters:
| Field | Type | Required | Description |
|---|---|---|---|
amount | number | Yes | Amount to deduct in BDT |
description | string | Yes | Human-readable description of the deduction |
metadata | object | No | Arbitrary key-value data for your records |
Success Response:
{
"wallet_id": 3,
"balance": 347.50,
"deducted": 2.50
}Insufficient Balance Error:
{
"error": "Insufficient wallet balance",
"code": "insufficient_balance",
"status": 400
}Overdraft Protection
The debit operation is atomic -- it uses a conditional SQL UPDATE that only succeeds if the balance is sufficient. This prevents overdraft race conditions even under concurrent requests. If two debit requests arrive simultaneously and only enough balance exists for one, exactly one will succeed and the other will receive the insufficient_balance error.
Transaction History
Retrieve a paginated list of all wallet transactions:
GET /api/apps/v1/billing/wallet/transactions?page=1&limit=20
Authorization: Bearer sx_at_...Query Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
page | integer | 1 | Page number |
limit | integer | 20 | Results per page (max 100) |
Response:
{
"data": [
{
"transaction_id": 45,
"type": "deduction",
"amount": 2.50,
"description": "SMS sent to +8801712345678",
"balance_after": 347.50,
"created_at": "2026-02-28T10:35:00.000Z"
},
{
"transaction_id": 44,
"type": "topup",
"amount": 500.00,
"description": "Wallet Top-up",
"balance_after": 500.00,
"created_at": "2026-02-28T09:00:00.000Z"
}
],
"pagination": {
"page": 1,
"limit": 20,
"total": 2
}
}Transaction types:
| Type | Description |
|---|---|
topup | Funds added via merchant payment |
deduction | Funds removed by app for service consumption |
refund | Funds returned to wallet (e.g., failed delivery) |
Example: SMS Billing with Wallet
async function sendSms(storeId, recipient, message, accessToken) {
// 1. Check wallet balance first
const walletRes = await fetch('https://api.selorax.io/api/apps/v1/billing/wallet', {
headers: { 'Authorization': `Bearer ${accessToken}` }
});
const wallet = await walletRes.json();
const smsCost = 2.50;
if (wallet.balance < smsCost) {
throw new Error('Insufficient wallet balance. Please ask the merchant to top up.');
}
// 2. Send the SMS
const smsResult = await smsProvider.send(recipient, message);
// 3. Debit the wallet
const debitRes = await fetch('https://api.selorax.io/api/apps/v1/billing/wallet/debit', {
method: 'POST',
headers: {
'Authorization': `Bearer ${accessToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
amount: smsCost,
description: `SMS sent to ${recipient}`,
metadata: { sms_id: smsResult.id, recipient }
})
});
if (debitRes.status === 400) {
const error = await debitRes.json();
if (error.code === 'insufficient_balance') {
// Race condition — balance was spent between check and debit
// Handle gracefully: queue for retry or notify merchant
}
}
return debitRes.json();
}Best Practices
- Always check the balance before attempting to debit. While the atomic debit operation prevents overdraft, checking first lets you show the merchant a helpful "top up" prompt rather than a raw error.
- Handle
insufficient_balancegracefully. Do not let the error bubble up as a generic 500 to the merchant. Instead, prompt them to top up their wallet with a direct link or apostMessagebilling redirect. - Log deductions with descriptive metadata. Merchants need to understand what they are paying for. Include identifiers (SMS IDs, order IDs, etc.) in the metadata so transactions are auditable.
- Consider low-balance alerts. When the wallet drops below a threshold (e.g., 10% of the average monthly spend), proactively notify the merchant to top up. This prevents service interruptions.
- Use the wallet for high-frequency, low-value transactions. If your app only charges once or twice a month, one-time charges or usage charges may be simpler.