SeloraXDEVELOPERS

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

  1. Your app creates a wallet top-up charge.
  2. The merchant approves and pays via the standard EPS payment flow.
  3. The payment amount is credited to the app-specific wallet for that store.
  4. 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:

FieldTypeRequiredDescription
namestringYesDisplay name shown to the merchant
amountnumberYesTop-up amount in BDT (min 10.00, max 50,000.00)
currencystringYesMust be "BDT"
return_urlstringNoURL 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
}
FieldDescription
wallet_idUnique identifier for this wallet
balanceCurrent available balance
currencyAlways "BDT"
total_topupLifetime total of all top-ups
total_spentLifetime 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:

FieldTypeRequiredDescription
amountnumberYesAmount to deduct in BDT
descriptionstringYesHuman-readable description of the deduction
metadataobjectNoArbitrary 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:

ParameterTypeDefaultDescription
pageinteger1Page number
limitinteger20Results 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:

TypeDescription
topupFunds added via merchant payment
deductionFunds removed by app for service consumption
refundFunds 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_balance gracefully. 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 a postMessage billing 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.