B2C Account Top Up
Move funds from your MMF/Working account into a B2C shortcode's utility account.
B2CTopUpClient wraps POST /mpesa/b2b/v1/paymentrequest with CommandID: "BusinessPayToBulk".
This is not a general B2C payment endpoint
Despite the URL living under /mpesa/b2b/..., this call moves money between
your own organization's accounts from your Working/MMF account into a B2C
shortcode's utility account, so that shortcode has funds to disburse to
customers later. It is not the endpoint you want for paying an arbitrary
customer directly; Daraja's B2C Payment Request (CustomerPayment-style
disbursement) is a separate flow this SDK does not currently implement see
Roadmap.
Request & response shapes
interface B2CTopUpRequest {
initiator: string; // API operator username with "Org Business Pay to Bulk API initiator" role
securityCredential: string; // pre-encrypted see warning below
senderShortCode: string; // your account PartyA, debited
receiverShortCode: string; // the B2C shortcode being topped up PartyB, credited
amount: number;
accountReference: string;
requester?: string; // optional MSISDN, format "254XXXXXXXXX"
remarks: string; // max 100 characters
queueTimeOutURL: string;
resultURL: string;
}
interface B2CTopUpResponse {
OriginatorConversationID: string;
ConversationID: string;
ResponseCode: string;
ResponseDescription: string;
}const accepted = await daraja.b2cTopUp.topUp({
initiator: "testapi",
securityCredential: encryptedSecurityCredential,
senderShortCode: "600979",
receiverShortCode: "600000",
amount: 239,
accountReference: "353353",
remarks: "Monthly float top up",
queueTimeOutURL: "https://example.com/callbacks/b2c-topup/timeout",
resultURL: "https://example.com/callbacks/b2c-topup/result",
});
console.log(accepted.ResponseDescription);securityCredential is not generated by this SDK
securityCredential must be your Initiator password, RSA-encrypted with
Safaricom's public certificate, then Base64-encoded. This SDK deliberately
does not perform that encryption for you it's a one-time setup step, not a
per-request operation, and doing it wrong silently produces a valid-looking
string that Daraja will reject with an opaque error. Generate it once using
Safaricom's certificate + encryption tool (linked from the Daraja docs' Test
Credentials page for sandbox, and from your Go-Live pack for production),
store the result as a secret, and pass it in as-is.
Validation the SDK performs
senderShortCode/receiverShortCodemust each be 5–6 digitsamountmust be a finite number > 0remarksrequired, non-empty, ≤ 100 charactersrequester, if provided, must match254XXXXXXXXXqueueTimeOutURLandresultURLeach go through callback URL validation
All failures throw errorCode: 'INVALID_B2C_TOPUP_REQUEST'.
The wire body (for reference)
If you ever need to compare against Safaricom's raw API docs or a network capture, this is exactly what gets sent note the field Safaricom itself misspells:
interface B2CTopUpWireRequest {
Initiator: string;
SecurityCredential: string;
CommandID: "BusinessPayToBulk";
SenderIdentifierType: "4";
RecieverIdentifierType: "4"; // sic Safaricom's own spelling, not a typo in this SDK
Amount: string; // note: sent as a string, unlike the public `amount: number` input
PartyA: string;
PartyB: string;
AccountReference: string;
Requester?: string;
Remarks: string;
QueueTimeOutURL: string;
ResultURL: string;
}Handling the result callback
topUp() only confirms Daraja accepted the request. The real outcome POSTs to resultURL later:
{
"Result": {
"ResultType": 0,
"ResultCode": 0,
"ResultDesc": "The service request has been accepted successfully.",
"OriginatorConversationID": "10571-7910404-1",
"ConversationID": "AG_20260822_0000circular123",
"TransactionID": "NLJ41HAY6Q",
"ResultParameters": {
"ResultParameter": [
{ "Key": "TransactionAmount", "Value": 239 },
{ "Key": "TransactionReceipt", "Value": "NLJ41HAY6Q" },
{ "Key": "B2CUtilityAccountAvailableFunds", "Value": 21634.35 },
{
"Key": "TransactionCompletedDateTime",
"Value": "22.08.2026 16:30:45"
}
]
}
}
}The SDK ships static helpers for parsing this on your webhook route so you don't hand-write the ResultParameter key/value flattening every time:
import { B2CTopUpClient } from "@lumierelabs/daraja";
import type { B2CTopUpResultCallback } from "@lumierelabs/daraja";
// In your resultURL handler, after parsing the request body:
const callback = B2CTopUpClient.asResultCallback(
requestBody,
) as B2CTopUpResultCallback;
if (B2CTopUpClient.isSuccessfulResult(callback.Result)) {
const params = B2CTopUpClient.parseResultParameters(callback.Result);
console.log(params.TransactionReceipt, params.TransactionAmount);
} else {
console.log("Top up failed:", callback.Result.ResultDesc);
}parseResultParameters() turns the awkward { Key, Value }[] array into a flat object { TransactionAmount: 239, TransactionReceipt: 'NLJ41HAY6Q', ... } so you don't .find() through an array for every field you need.