Wallet
Your account carries three balances: money you have topped up, SMS credits for texting, and voice credits for calling. They are counted in different units and are never added together.
| Money | SMS credits | Voice credits | |
|---|---|---|---|
| Unit | Cedis (GHS) | Segments | Seconds of call time |
| Arrives from | A mobile-money top-up | Buying an SMS bundle | Buying a voice bundle |
| Pays for | Bundles only | Sending messages | Placing calls |
| Expires | Never | Depends on the bundle | Depends on the bundle |
What pays for a send
SMS credits, and nothing else. Credits are spent soonest-to-expire first. Your wallet is never charged per message — it exists to buy bundles.
A send with too few credits is refused with 402 INSUFFICIENT_BALANCE, however much money is in the wallet. That is deliberate: if cedis paid for sends once credits ran out, the wallet would simply be a second SMS balance and the separation would mean nothing.
One credit is one segment, not one message. A long or emoji-bearing message costs several. See Messages for how segments are counted.
What pays for a call
Voice credits, counted in seconds. A call costs its recording's length once per person reached — a 30-second message to 100 people is 3,000 seconds. SMS credits cannot pay for a call and call time cannot send a text, so an account with thousands of SMS credits and no voice credit is refused a voice campaign.
Voice bundles are sold in hours and spent in seconds. Read your voice balance at GET /api/v1/voice/balance, and see Voice calls for how a call is billed.
Checking your balances
/api/v1/wallet{
"success": true,
"data": {
"balance": "149.64",
"reserved": "1.20",
"available": "148.44",
"currency": "GHS",
"credits": {
"balance": 4820,
"reserved": 6,
"available": 4814,
"lots": [
{
"id": "clot_9f2b...",
"remaining": 820,
"purchased": 1000,
"expires_at": "2026-11-04T00:00:00.000Z"
},
{
"id": "clot_3a71...",
"remaining": 4000,
"purchased": 5000,
"expires_at": null
}
]
},
"estimated_sms_remaining": 4814
}
}| Field | Meaning |
|---|---|
balance | Settled money. |
reserved | Money held against messages queued or scheduled but not yet submitted. |
available | balance minus reserved. What you can commit to a new send. |
credits.balance | SMS credits held, in segments. |
credits.reserved | Credits held against in-flight sends. A subset of credits.balance, not extra. |
credits.lots | Each block of credits you bought, soonest expiry first. expires_at: null means it never expires. |
estimated_sms_remaining | How many more segments you can send. Equal to credits.available — wallet money is not included, because it cannot pay for a send. |
Bundles
/api/v1/wallet/packagesThe bundles currently on sale. Prices and sizes are set by us and can change.
{
"success": true,
"data": [
{
"id": "bndl_7c1e...",
"name": "Business",
"slug": "business-bundle",
"price": "250.00",
"sms_count": 5000,
"price_per_sms": "0.0500",
"validity_days": 180,
"never_expires": false,
"description": "For regular senders.",
"currency": "GHS"
},
{
"id": "bndl_22af...",
"name": "Lifetime 10K",
"slug": "lifetime-10k",
"price": "600.00",
"sms_count": 10000,
"price_per_sms": "0.0600",
"validity_days": null,
"never_expires": true,
"description": "Never expires, at a better rate.",
"currency": "GHS"
}
]
}Buying one
/api/v1/wallet/bundles/{id}/purchasecurl -X POST https://api.jkasms.com/api/v1/wallet/bundles/bndl_7c1e.../purchase \
-H "Authorization: Bearer $JKASMS_SECRET_KEY" \
-H "X-JKASMS-Org-Id: $JKASMS_ORG_ID" \
-H "Idempotency-Key: $(uuidgen)"{
"success": true,
"data": {
"lot_id": "clot_9f2b...",
"credits": 5000,
"paid": "250.00",
"currency": "GHS",
"expires_at": "2027-03-22T00:00:00.000Z"
}
}This spends money already in your wallet — it is not a card or mobile-money charge, so there is no pending state to poll. The debit and the credit grant happen together: either both apply or neither does. Returns 402 if your available money does not cover the price.
Transactions
Money ledger
/api/v1/wallet/transactionsEvery movement is recorded with the balance after it.
| Type | Meaning |
|---|---|
TOPUP | You added funds. |
BUNDLE | You spent money on SMS credits. |
RESERVE | Money held for a queued send. |
RELEASE | Hold returned — the message was never submitted. |
USAGE | A message was accepted by a network and billed. |
REFUND | A billed message was rejected or expired. |
ADJUSTMENT | Manual correction, always with a reason. |
Credit ledger
/api/v1/wallet/credits/transactionsThe same shape, counted in segments rather than cedis. The credits field is signed; a hold is 0, because reserving moves credits between available and reserved without changing the total.
| Type | Meaning |
|---|---|
PURCHASE | You bought a bundle. |
RESERVE | Credits held for a queued send. |
RELEASE | Hold returned — the message was never submitted. |
USAGE | Credits spent on an accepted message. |
REFUND | Credits returned for a message that was billed and then rejected or expired. |
EXPIRY | A bundle reached its expiry date with credits unused. |
ADJUSTMENT | Manual correction, always with a reason. |
EXPIRY rows, so you can always see exactly when and how many you lost. Watch credits.lots[].expires_at if you want to warn your own users first.Topping up
/api/v1/wallet/topupcurl -X POST https://api.jkasms.com/api/v1/wallet/topup \
-H "Authorization: Bearer $JKASMS_SECRET_KEY" \
-H "X-JKASMS-Org-Id: $JKASMS_ORG_ID" \
-H "Content-Type: application/json" \
-d '{ "amount": "100.00", "msisdn": "0241234567", "network": "MTN" }'Returns 202: a payment prompt has been sent to that phone. Your balance updates only once payment is confirmed server-side — never on the strength of a browser or client callback.
This adds money. To turn it into SMS credits at a lower rate per message, buy a bundle with it.