Daraja SDK Logomark
Daraja SDK
Endpoints

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 / receiverShortCode must each be 5–6 digits
  • amount must be a finite number > 0
  • remarks required, non-empty, ≤ 100 characters
  • requester, if provided, must match 254XXXXXXXXX
  • queueTimeOutURL and resultURL each 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.

On this page