M-Pesa B2C payouts: initiator, security credential and results explained
What you need to send M-Pesa B2C payments through Daraja - the B2C shortcode, initiator, security credential and certificate - how results come back, and how to pay out without ever sending money twice.
· 8 min read · Noria Technologies
Sending money out over M-Pesa, to pay staff, refund customers or settle agents, uses Safaricom’s B2C (business to customer) API. It is harder than STK push: it needs credentials most teams have never handled, it answers asynchronously, and a mistake moves real money out of your account.
This guide explains what B2C needs, what each credential is for, how results come back, and how to make payouts safe to retry.
What you need before your first payout
| Item | What it is |
|---|---|
| A B2C shortcode | A paybill enabled for B2C, funded with the money you will pay out. Collections and B2C are separate capabilities. |
| A Daraja app | Consumer key and secret for that shortcode, for OAuth tokens. |
| An initiator | A user created on the M-Pesa organisation portal with permission to make B2C payments, with its own name and password. |
| Safaricom’s public certificate | Used to encrypt the initiator password. Sandbox and production use different certificates. |
| Result and timeout URLs | Public HTTPS URLs where Safaricom sends the outcome. |
The security credential
Daraja never accepts the initiator password in plain text. Instead you send a SecurityCredential: the initiator password, encrypted with Safaricom’s public certificate using RSA with PKCS#1 v1.5 padding, then base64-encoded.
In Go, the essential step looks like this:
block, _ := pem.Decode(certificatePEM)
cert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
return "", err
}
key := cert.PublicKey.(*rsa.PublicKey)
cipher, err := rsa.EncryptPKCS1v15(rand.Reader, key, []byte(initiatorPassword))
if err != nil {
return "", err
}
return base64.StdEncoding.EncodeToString(cipher), nil
Two things catch people out. The production certificate is not the sandbox one, so a credential built with the wrong certificate fails with an invalid-initiator error. And the initiator password expires on the portal like any other password, after which every payout fails until it is changed and the credential rebuilt.
The request
A B2C request names the shortcode paying, the phone number receiving, the amount in whole shillings, and a CommandID describing the payment:
BusinessPayment- general payments, such as refunds or supplier payments.SalaryPayment- salaries and wages.PromotionPayment- promotions and rewards.
Daraja answers immediately with a ConversationID. As with STK push, that only means Safaricom accepted the request. The outcome comes later.
How results come back
Safaricom posts the outcome to your ResultURL. A ResultCode of 0 means the money was sent, and the result includes the M-Pesa TransactionID, the amount and the receiving party. Any other code is a failure with a ResultDesc.
If Safaricom cannot process the request in time, it posts to your QueueTimeOutURL instead. A timeout does not mean the payment failed. It means Safaricom did not answer in time, and the money may or may not have moved.
Making payouts safe to retry
A duplicated STK push asks the customer twice. A duplicated B2C payment sends money twice, and getting it back is slow and awkward. So:
- Give every payout an idempotency key tied to your own record, such as
payout-2207, and refuse to send the same key twice. - Treat timeouts and missing results as unknown. Never send a replacement payout while one is unknown.
- Resolve unknown payouts by asking Safaricom with the transaction status API, or by checking the organisation statement, before deciding anything.
- Store the M-Pesa
TransactionIDas unique, so a repeated result cannot be recorded twice. - Never let a callback mark money going out as failed on its own if you cannot authenticate it. A forged failure followed by a manual resend is a payout sent twice.
The guide on idempotency keys covers the first three in depth.
How Noria Pay sends payouts
Noria Pay payouts handle B2C, and payouts through SasaPay and KCB Buni, behind one API:
- Upload Safaricom’s certificate and your initiator credentials once. They are encrypted and bound to their record, and Pay builds the security credential for every B2C payment and reversal.
- Every payout requires an idempotency key. The same key returns the original payout; a different body under the same key is refused.
- Each payout gets its own secret result URL, so a result can only settle the payout it belongs to.
- A queue timeout marks the payout unknown, and Pay never sends it again. It stays open until the provider’s result, a status check where the provider offers one, or your statement settles it.
- Results are recorded once, and your systems get a signed webhook for each change.
curl https://pay.noria.co.ke/v1/payouts \
-H "Authorization: Bearer $NORIA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: payout-2207" \
-d '{
"amount_minor": 150000,
"currency": "KES",
"channel": "mpesa",
"recipient_name": "Jay Kiharani",
"recipient_phone": "254712345678",
"reference": "PAYOUT-2207",
"description": "SiteBooks site wages"
}'
The same request shape sends money to Airtel Money, banks and wallets through SasaPay or KCB Buni, by changing the channel and recipient.
Checklist
- B2C shortcode funded, initiator created and its password tracked for expiry.
- Security credential built with the right certificate for the environment.
- Result and timeout URLs public, HTTPS and free of filtered words.
- Every payout carries an idempotency key.
- Timeouts treated as unknown and resolved before any resend.
- Transaction IDs stored as unique.