Wallet-as-a-Service (WaaS)
Give each of your end users a virtual account while real money sits in one master business wallet. Covers accounts, deposits, withdrawals, transfers and events.
https://api.paysasa.com/api/v1Authx-api-keyFormatJSONWaaS uses one master business wallet on Paysasa while tracking virtual sub-accounts for your end-users. Real money moves only through the master wallet; you maintain your own internal user balances and update them from WaaS webhooks.
Enable WaaS under PATCH /businesses/:id/waas (dashboard session or API key). All WaaS routes require business_id when using API keys. Business must be verified (default gate) — see Compliance & Trust.
Deposit channels: toggle manual_deposits_enabled (PayBill / SIM toolkit to VA ref) and api_deposits_enabled (intent / checkout / STK). Both default on. When manual is off, C2B validation rejects and confirmation refunds via M-Pesa Transaction Reversal (B2C fallback to payer).
Optional VA display_name can appear in owner deposit and withdrawal SMS when those notification events are enabled.
Each VA has two PayBill account references customers can use on M-Pesa: mpesa_account_number (e.g. WAAS-MATHEW) and serial_account_number (e.g. 12345678). Both credit the same virtual account on C2B confirmation.
Merchant visibility: Use GET /businesses/:id/waas for master wallet balance, spendable, reserved, and deposit-channel flags. Use GET /waas/virtual-accounts?include_ledger=1 for each end-user attributed balance, and GET /waas/transactions?external_id=… for per-user deposit/withdrawal history.
API endpoints#
| Method | Path | Description |
|---|---|---|
| GET/PATCH | /businesses/:id/waas | WaaS settings (enable, reserved, manual/API deposit toggles) |
| GET/POST | /waas/virtual-accounts | List/create VAs (?include_ledger=1). Body: external_id, display_name?, display_identifier?. Response includes mpesa_account_number + serial_account_number |
| GET/PATCH | /waas/virtual-accounts/:id | Get/update VA (status, display_name, display_identifier; ?include_transactions=1) |
| POST | /waas/deposits/intent | Time-bound PayBill deposit intent |
| GET | /waas/deposits/intent/:id | Deposit intent status |
| POST | /waas/deposits/checkout | Hosted checkout session (wallet + M-Pesa) |
| POST | /waas/deposits/stk | Checkout + one-shot M-Pesa STK |
| POST | /waas/withdraw | Withdraw to M-Pesa or Paysasa wallet (fee_bearer: master|va) |
| GET | /waas/withdraw/quote | Quote fee + debits (?amount=&fee_bearer=) |
| GET | /waas/withdraw/status | Poll withdrawal (?transaction_id= or ?reference=) |
| GET | /waas/transactions | WaaS tx history (?external_id=, ?virtual_account_id=, ?type=) |
| GET | /waas/reconciliation | Master wallet vs aggregates snapshot |
| POST | /waas/sandbox/simulate-deposit | Sandbox-only simulated C2B (test API keys) |
curl "https://api.paysasa.com/api/v1/businesses/biz_xxxxxxxxxxxxxxxx/waas" \
-H "x-api-key: apk_live_<prefix>.<secret>"
# → waas: { enabled, balance_minor, spendable_balance_minor, reserved_balance_minor,
# manual_deposits_enabled, api_deposits_enabled, ... }# List virtual accounts with attributed balances
curl "https://api.paysasa.com/api/v1/waas/virtual-accounts?business_id=biz_xxxxxxxxxxxxxxxx&include_ledger=1&limit=50" \
-H "x-api-key: apk_live_<prefix>.<secret>"
# virtual_accounts[].mpesa_account_number → e.g. WAAS-MATHEW
# virtual_accounts[].serial_account_number → e.g. 22457777 (numeric PayBill ref)# Single virtual account + balance + transactions
curl "https://api.paysasa.com/api/v1/waas/virtual-accounts/va_xxxxxxxxxxxxxxxx?business_id=biz_xxxxxxxxxxxxxxxx&include_ledger=1&include_transactions=1&tx_limit=50" \
-H "x-api-key: apk_live_<prefix>.<secret>"curl -X PATCH "https://api.paysasa.com/api/v1/waas/virtual-accounts/va_xxxxxxxxxxxxxxxx" \
-H "x-api-key: apk_live_<prefix>.<secret>" \
-H "Content-Type: application/json" \
-d '{
"business_id": "biz_xxxxxxxxxxxxxxxx",
"status": "suspended",
"display_identifier": "254712345678",
"display_name": "Mathew Doe"
}'curl -X PATCH "https://api.paysasa.com/api/v1/businesses/biz_xxxxxxxxxxxxxxxx/waas" \
-H "x-api-key: apk_live_<prefix>.<secret>" \
-H "Content-Type: application/json" \
-d '{
"enabled": true,
"reserved_balance_minor": 100000,
"manual_deposits_enabled": true,
"api_deposits_enabled": true
}'curl -X POST "https://api.paysasa.com/api/v1/waas/virtual-accounts" \
-H "x-api-key: apk_live_<prefix>.<secret>" \
-H "Content-Type: application/json" \
-d '{
"business_id": "biz_xxxxxxxxxxxxxxxx",
"external_id": "pollsoko_user_992",
"display_name": "Mathew Doe",
"display_identifier": "254712345678"
}'
# → virtual_account: {
# mpesa_account_number: "WAAS-POLLSOKO_USER_992",
# serial_account_number: "22457777",
# ...
# }curl -X POST "https://api.paysasa.com/api/v1/waas/deposits/intent" \
-H "x-api-key: apk_live_<prefix>.<secret>" \
-H "Content-Type: application/json" \
-d '{
"business_id": "biz_xxxxxxxxxxxxxxxx",
"external_id": "pollsoko_user_992",
"amount": 50000,
"ttl_minutes": 30
}'# Hosted checkout (STK on /checkout/{sessionId} or server-side STK below)
curl -X POST "https://api.paysasa.com/api/v1/waas/deposits/checkout" \
-H "x-api-key: apk_live_<prefix>.<secret>" \
-H "Content-Type: application/json" \
-d '{
"business_id": "biz_xxxxxxxxxxxxxxxx",
"external_id": "pollsoko_user_992",
"amount": 50000,
"customer_phone": "254712345678",
"success_url": "https://pollsoko.com/wallet/topup/success"
}'# One-shot: create checkout session + trigger M-Pesa STK push
curl -X POST "https://api.paysasa.com/api/v1/waas/deposits/stk" \
-H "x-api-key: apk_live_<prefix>.<secret>" \
-H "Content-Type: application/json" \
-d '{
"business_id": "biz_xxxxxxxxxxxxxxxx",
"external_id": "pollsoko_user_992",
"amount": 50000,
"phone": "254712345678"
}'# Withdraw to M-Pesa (B2C)
curl -X POST "https://api.paysasa.com/api/v1/waas/withdraw" \
-H "x-api-key: apk_live_<prefix>.<secret>" \
-H "x-idempotency-key: wdrl-unique-key-001" \
-H "Content-Type: application/json" \
-d '{
"business_id": "biz_xxxxxxxxxxxxxxxx",
"virtual_account_id": "va_xxxxxxxxxxxxxxxx",
"amount": 100000,
"destination_type": "mpesa",
"destination_phone": "254712345678"
}'# Withdraw to a Paysasa user wallet (settles as an in-platform transfer)
curl -X POST "https://api.paysasa.com/api/v1/waas/withdraw" \
-H "x-api-key: apk_live_<prefix>.<secret>" \
-H "x-idempotency-key: wdrl-wallet-001" \
-H "Content-Type: application/json" \
-d '{
"business_id": "biz_xxxxxxxxxxxxxxxx",
"source_identifier": "pollsoko_user_992",
"amount": 100000,
"destination_type": "wallet",
"destination_wallet": "254712345678"
}'curl "https://api.paysasa.com/api/v1/waas/withdraw/status?business_id=biz_xxxxxxxxxxxxxxxx&transaction_id=tx_xxxxxxxxxxxxxxxx" \
-H "x-api-key: apk_live_<prefix>.<secret>"# All WaaS transactions, or filter by end-user
curl "https://api.paysasa.com/api/v1/waas/transactions?business_id=biz_xxxxxxxxxxxxxxxx&external_id=pollsoko_user_992&type=waas_deposit&limit=25" \
-H "x-api-key: apk_live_<prefix>.<secret>"
# Or filter by virtual_account_id
curl "https://api.paysasa.com/api/v1/waas/transactions?business_id=biz_xxxxxxxxxxxxxxxx&virtual_account_id=va_xxxxxxxxxxxxxxxx&limit=25" \
-H "x-api-key: apk_live_<prefix>.<secret>"curl "https://api.paysasa.com/api/v1/waas/reconciliation?business_id=biz_xxxxxxxxxxxxxxxx" \
-H "x-api-key: apk_live_<prefix>.<secret>"# Sandbox API keys only — simulates C2B without real M-Pesa
curl -X POST "https://api.paysasa.com/api/v1/waas/sandbox/simulate-deposit" \
-H "x-api-key: apk_test_<prefix>.<secret>" \
-H "Content-Type: application/json" \
-d '{
"business_id": "biz_xxxxxxxxxxxxxxxx",
"external_id": "pollsoko_user_992",
"amount": 50000
}'Webhook events: waas.deposit.success, waas.withdrawal.success, and failure variants. Withdrawal webhooks include destination_type (mpesa or wallet).