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.

MoneySMS creditsVoice credits
UnitCedis (GHS)SegmentsSeconds of call time
Arrives fromA mobile-money top-upBuying an SMS bundleBuying a voice bundle
Pays forBundles onlySending messagesPlacing calls
ExpiresNeverDepends on the bundleDepends on the bundle
A top-up buys money, not messages. Converting money into credits is a separate step — buying a bundle. Until you do, the money sits in your wallet and you cannot send.

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

GET/api/v1/wallet
Response200
JSON
{
  "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
  }
}
FieldMeaning
balanceSettled money.
reservedMoney held against messages queued or scheduled but not yet submitted.
availablebalance minus reserved. What you can commit to a new send.
credits.balanceSMS credits held, in segments.
credits.reservedCredits held against in-flight sends. A subset of credits.balance, not extra.
credits.lotsEach block of credits you bought, soonest expiry first. expires_at: null means it never expires.
estimated_sms_remainingHow many more segments you can send. Equal to credits.available — wallet money is not included, because it cannot pay for a send.
Credit is reserved when you send and only spent when a network accepts the message. A rejected message releases its reservation, so you are never charged for something that never left.

Bundles

GET/api/v1/wallet/packages

The bundles currently on sale. Prices and sizes are set by us and can change.

Response200
JSON
{
  "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

POST/api/v1/wallet/bundles/{id}/purchase
Shell
curl -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)"
Response201
JSON
{
  "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.

Editing a bundle never changes credits already bought. Each purchase records its own size, price and expiry at the moment of sale, so a later price change never revalues what you are holding.

Transactions

Money ledger

GET/api/v1/wallet/transactions

Every movement is recorded with the balance after it.

TypeMeaning
TOPUPYou added funds.
BUNDLEYou spent money on SMS credits.
RESERVEMoney held for a queued send.
RELEASEHold returned — the message was never submitted.
USAGEA message was accepted by a network and billed.
REFUNDA billed message was rejected or expired.
ADJUSTMENTManual correction, always with a reason.

Credit ledger

GET/api/v1/wallet/credits/transactions

The 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.

TypeMeaning
PURCHASEYou bought a bundle.
RESERVECredits held for a queued send.
RELEASEHold returned — the message was never submitted.
USAGECredits spent on an accepted message.
REFUNDCredits returned for a message that was billed and then rejected or expired.
EXPIRYA bundle reached its expiry date with credits unused.
ADJUSTMENTManual correction, always with a reason.
Expiring credits are written to this ledger as real 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

POST/api/v1/wallet/topup
Shell
curl -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.