One-Time Charges
One-Time Charges
One-time charges let you collect a single payment from a merchant. Use them for setup fees, content packs, credit bundles, or any non-recurring purchase.
Charge Flow
Step 1: Create the Charge
Send a POST request from your app's backend:
POST /api/apps/v1/billing/charges
Authorization: Bearer sx_at_...
Content-Type: application/json
{
"name": "SMS Pack 100",
"description": "100 SMS credits",
"amount": 150.00,
"currency": "BDT",
"return_url": "https://app.example.com/billing/success",
"metadata": { "sku": "sms-100" },
"idempotency_key": "unique-key-123"
}Parameters:
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Display name shown to the merchant |
description | string | No | Additional details about the charge |
amount | number | Yes | Amount in BDT (min 10.00, max 50,000.00) |
currency | string | Yes | Must be "BDT" |
return_url | string | No | URL to redirect the merchant after payment |
metadata | object | No | Arbitrary key-value data stored with the charge |
idempotency_key | string | No | Unique key to prevent duplicate charges |
Response:
{
"charge_id": 5,
"name": "SMS Pack 100",
"amount": 150.00,
"base_amount": 150.00,
"currency": "BDT",
"fee_payer": "developer",
"commission_rate": 0.1,
"platform_amount": 15.00,
"gateway_fee_rate": 0.025,
"gateway_fee_amount": 3.75,
"developer_amount": 131.25,
"status": "pending",
"confirmation_url": "/22/settings/apps/billing/5",
"created_at": "2026-02-28T10:00:00.000Z"
}The amount field is what the merchant pays. When fee_payer is developer, amount equals base_amount. When fee_payer is merchant, amount = base_amount + platform_amount + gateway_fee_amount.
Step 2: Redirect the Merchant to Approval
Since your app runs inside an iframe in the SeloraX dashboard, you cannot navigate directly. Instead, use postMessage to tell the parent dashboard to navigate the merchant to the charge approval page:
window.parent.postMessage({
type: 'selorax:billing-redirect',
url: confirmation_url
}, '*');The dashboard will navigate the merchant to the charge approval page, which displays the charge name, description, amount, and your app's information.
Step 3: Merchant Approves
When the merchant clicks "Approve & Pay" on the approval page:
- The dashboard calls
POST /api/apps/billing/charges/:id/approve. - The platform creates an EPS payment session with a unique
merchant_transaction_id. - The merchant is redirected to the EPS payment gateway to complete payment.
Step 4: Payment Completes
After the merchant completes payment on the EPS gateway:
- EPS redirects to
GET /api/apps/billing/callback?session_txn=...&status=success. - The platform verifies the transaction directly with EPS to confirm payment.
- The charge status is updated to
active. - A
charge.activatedwebhook is fired to your app. - The merchant is redirected to one of:
- Your
return_url(if provided):{return_url}?payment=success&charge_id=5 - The default billing complete page:
/{store_id}/settings/apps/billing/complete?payment=success
- Your
Decline Flow
If the merchant clicks "Decline" on the approval page:
- The charge status is set to
declined. - A
charge.declinedwebhook is fired to your app. - The merchant is redirected back to your app or the apps list.
Expiry
If the merchant takes no action within 48 hours of charge creation:
- The charge status is automatically set to
expired. - A
charge.expiredwebhook is fired to your app.
Idempotency
To prevent duplicate charges (for example, if a network error causes a retry), include an idempotency_key in the request body or an Idempotency-Key header:
POST /api/apps/v1/billing/charges
Idempotency-Key: order-123-sms-packIf a charge with the same idempotency key already exists for your app and store, the existing charge is returned instead of creating a new one.
Webhooks
| Event | Fired When |
|---|---|
charge.activated | Merchant approved and payment succeeded |
charge.declined | Merchant explicitly declined |
charge.expired | No action within 48 hours |
See the Webhooks documentation for details on receiving and verifying webhook payloads.
Example: Complete Flow
// 1. Create charge from your backend
const response = await fetch('https://api.selorax.io/api/apps/v1/billing/charges', {
method: 'POST',
headers: {
'Authorization': `Bearer ${accessToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
name: 'SMS Pack 100',
description: '100 SMS credits',
amount: 150.00,
currency: 'BDT',
return_url: 'https://app.example.com/billing/callback',
idempotency_key: `sms-pack-${storeId}-${Date.now()}`
})
});
const charge = await response.json();
// 2. Send confirmation_url to your frontend iframe
// Your frontend then does:
window.parent.postMessage({
type: 'selorax:billing-redirect',
url: charge.confirmation_url
}, '*');
// 3. Handle the return_url callback
// When merchant is redirected back to your return_url:
// https://app.example.com/billing/callback?payment=success&charge_id=5
const params = new URLSearchParams(window.location.search);
if (params.get('payment') === 'success') {
// Charge was successful — verify via API or wait for webhook
}