Safaricom.Daraja
0.2.0
dotnet add package Safaricom.Daraja --version 0.2.0
NuGet\Install-Package Safaricom.Daraja -Version 0.2.0
<PackageReference Include="Safaricom.Daraja" Version="0.2.0" />
<PackageVersion Include="Safaricom.Daraja" Version="0.2.0" />
<PackageReference Include="Safaricom.Daraja" />
paket add Safaricom.Daraja --version 0.2.0
#r "nuget: Safaricom.Daraja, 0.2.0"
#:package Safaricom.Daraja@0.2.0
#addin nuget:?package=Safaricom.Daraja&version=0.2.0
#tool nuget:?package=Safaricom.Daraja&version=0.2.0
Safaricom.Daraja
An idiomatic .NET SDK for Safaricom's Daraja API, with first-class Sandbox/Production environment switching and one shared OAuth session across every product area.
Authored by Modi97 / Jose Modi.
Status: early preview (v0.2.0), published to NuGet.org. All four packages below are implemented and tested, including live verification against the real Daraja Sandbox — see Verified against the live Sandbox.
Coverage
| Area | Status |
|---|---|
| OAuth (client credentials) | ✅ |
| M-Pesa Express (STK Push + Query) | ✅ |
| Customer to Business (C2B register + simulate) | ✅ |
| Business to Customer (B2C, incl. B2Pochi) | ✅ |
| Business to Business (B2B, incl. Tax Remittance) | ✅ |
| Transaction Reversal | ✅ |
| Transaction Status Query | ✅ |
| Account Balance | ✅ |
| M-Pesa Ratiba (standing orders) | ✅ |
| Lipa na Bonga | ✅ (raw JSON — undocumented product, see note below) |
| Pull Transactions API | ✅ |
| Dynamic QR Code | ✅ |
| Bill Manager (opt-in, invoicing, cancellation) | ✅ |
| ASP.NET Core DI + webhook mapping | ✅ |
CLI tool (dotnet-safaricom-daraja) |
✅ |
IoT SIM-portal / IMSI (Safaricom.Daraja.IoT) |
✅ (raw JSON — undocumented product, see note below) |
Installation
dotnet add package Safaricom.Daraja
dotnet add package Safaricom.Daraja.AspNetCore # optional: DI + webhook mapping
dotnet add package Safaricom.Daraja.IoT # optional: SIM-portal / IMSI (separate product, see below)
dotnet tool install --global dotnet-safaricom-daraja # optional: CLI setup wizard
Quickstart
using Safaricom.Daraja;
using Safaricom.Daraja.Models;
var client = new DarajaClient(new DarajaConfig
{
Environment = DarajaEnvironment.Sandbox, // or DarajaEnvironment.Production
ConsumerKey = "YOUR_CONSUMER_KEY",
ConsumerSecret = "YOUR_CONSUMER_SECRET",
});
// Or via environment variables: DARAJA_CONSUMER_KEY, DARAJA_CONSUMER_SECRET, DARAJA_ENVIRONMENT
// var client = new DarajaClient(new DarajaConfig());
Every service is a property on DarajaClient, sharing one cached OAuth token underneath:
STK Push (Lipa na M-Pesa Online)
var push = await client.StkPush.PushAsync(new StkPushRequest
{
ShortCode = "174379",
Passkey = "YOUR_LIPA_NA_MPESA_PASSKEY",
TransactionType = StkTransactionType.CustomerPayBillOnline,
Amount = 1,
PhoneNumber = "254712345678",
CallBackURL = "https://example.com/mpesa/stk-callback",
AccountReference = "INV-0001",
TransactionDesc = "Order payment",
});
var status = await client.StkPush.QueryAsync(new StkPushQueryRequest
{
ShortCode = "174379",
Passkey = "YOUR_LIPA_NA_MPESA_PASSKEY",
CheckoutRequestId = push.CheckoutRequestId!,
});
C2B
await client.C2B.RegisterUrlAsync(new C2BRegisterUrlRequest
{
ShortCode = "600000",
ResponseType = nameof(C2BResponseType.Completed),
// Daraja rejects ValidationURL/ConfirmationURL values containing the word "mpesa" with a
// 400 ("Bad Request - Invalid ValidationURL - URL has the word MPESA") - confirmed live
// against the Sandbox. Avoid it in these two URLs specifically.
ConfirmationURL = "https://example.com/payments/c2b/confirmation",
ValidationURL = "https://example.com/payments/c2b/validation",
});
// Sandbox only: simulate a customer paying in
await client.C2B.SimulateAsync(new C2BSimulateRequest
{
ShortCode = "600000",
CommandID = nameof(C2BCommandId.CustomerPayBillOnline),
Amount = "100",
Msisdn = "254712345678",
BillRefNumber = "INV-0001",
});
B2C / B2B / Reversal / Transaction Status / Account Balance
These all require a SecurityCredential — the Initiator password RSA-encrypted with
Safaricom's public certificate:
using Safaricom.Daraja.Security;
// Download YOUR environment's certificate from the Daraja portal (Sandbox and
// Production certificates differ) — see the security note below.
var securityCredential = SecurityCredentialEncryptor.EncryptFromCertificateFile(
"certs/production-cert.cer",
"YourInitiatorPassword");
await client.B2C.SendAsync(new B2CRequest
{
InitiatorName = "apiuser",
SecurityCredential = securityCredential,
CommandID = nameof(B2CCommandId.BusinessPayment),
Amount = "500",
PartyA = "600000",
PartyB = "254712345678",
Remarks = "Refund",
QueueTimeOutURL = "https://example.com/mpesa/b2c/timeout",
ResultURL = "https://example.com/mpesa/b2c/result",
});
// Tax Remittance is the same B2B endpoint under the hood:
await client.B2B.PayTaxesAsync(
initiator: "apiuser",
securityCredential: securityCredential,
businessShortCode: "600000",
kraShortCode: "YOUR_KRA_SHORTCODE_FROM_DARAJA_PORTAL",
paymentRegistrationNumber: "PRN123456",
amount: 1000,
remarks: "VAT remittance",
queueTimeOutUrl: "https://example.com/mpesa/b2b/timeout",
resultUrl: "https://example.com/mpesa/b2b/result");
The Result (success/failure, amounts, transaction IDs) for all of the above arrives
asynchronously as a callback posted to your ResultURL — model it with
Safaricom.Daraja.Models.DarajaResultCallback when you build the receiving endpoint.
M-Pesa Ratiba (standing orders)
await client.Ratiba.CreateAsync(new RatibaCreateRequest
{
StandingOrderName = "Monthly subscription",
BusinessShortCode = "174379",
TransactionType = "Standing Order Customer Pay Bill",
Amount = "500",
PartyA = "254712345678",
ReceiverPartyIdentifierType = ((int)RatibaReceiverType.Paybill).ToString(),
CallBackURL = "https://example.com/mpesa/ratiba-callback",
AccountReference = "SUB-001",
TransactionDesc = "Monthly subscription",
Frequency = ((int)RatibaFrequency.Monthly).ToString(),
StartDate = "2026-10-01",
EndDate = "2027-10-01",
});
Dynamic QR Code
var qr = await client.QrCode.GenerateAsync(new QrCodeRequest
{
MerchantName = "Example Store",
ReferenceNo = "INV-0001",
Amount = 500,
TransactionCode = QrTransactionCode.PayBill,
CreditPartyIdentifier = "174379",
});
System.IO.File.WriteAllBytes("qr.png", qr.ToPngBytes());
Bill Manager
await client.BillManager.OptInAsync(new BillManagerOptInRequest
{
ShortCode = "174379",
Email = "billing@example.com",
OfficialContact = "254712345678",
SendReminders = 1,
CallbackUrl = "https://example.com/mpesa/billmanager/reconciliation",
});
await client.BillManager.SendSingleInvoiceAsync(new BillManagerInvoice
{
ExternalReference = "INV-0002",
BilledFullName = "Jane Doe",
BilledPhoneNumber = "254712345678",
BilledPeriod = "2026-09-01",
InvoiceName = "September rent",
DueDate = "2026-09-30",
AccountReference = "RENT-0002",
Amount = 15000,
InvoiceItems = new() { new BillManagerInvoiceItem { ItemName = "Rent", Amount = 15000 } },
});
ASP.NET Core: DI + webhook mapping
using Safaricom.Daraja;
using Safaricom.Daraja.AspNetCore;
using Safaricom.Daraja.Models;
var builder = WebApplication.CreateBuilder(args);
// From appsettings.json section "Daraja" (ConsumerKey, ConsumerSecret, Environment, BaseAddress),
// or from DARAJA_* environment variables:
builder.Services.AddDaraja(builder.Configuration);
var app = builder.Build();
// STK Push callback
app.MapStkCallback("/mpesa/stk-callback", async (callback, ctx) =>
{
if (callback.IsSuccess)
{
var receipt = callback.GetMetadataValue("MpesaReceiptNumber");
// await orders.MarkPaidAsync(callback.CheckoutRequestId, receipt);
}
});
// C2B: Validation (accept/reject) and Confirmation (informational, already settled)
app.MapC2BValidation("/mpesa/c2b/validation", async (callback, ctx) =>
{
var known = await accounts.ExistsAsync(callback.BillRefNumber);
return known ? C2BValidationResponse.Accept() : C2BValidationResponse.Reject("Unknown account");
});
app.MapC2BConfirmation("/mpesa/c2b/confirmation", async (callback, ctx) =>
{
// await ledger.RecordAsync(callback);
});
// Shared Result envelope for B2C / B2B / Reversal / Transaction Status / Account Balance —
// map the same handler at both ResultURL and QueueTimeOutURL.
app.MapResultCallback("/mpesa/b2c/result", async (result, ctx) =>
{
if (result.IsSuccess)
{
var transactionId = result.TransactionId;
// ...
}
});
// Bill Manager reconciliation (posted to the callback URL you gave OptInAsync)
app.MapBillManagerReconciliation("/mpesa/billmanager/reconciliation", async (payload, ctx) =>
{
// await invoices.MarkPaidAsync(payload.ExternalReference, payload.PaidAmount);
});
app.Run();
Each Map* helper disables antiforgery on that route (these are server-to-server callbacks, not
browser form posts) and returns the acknowledgement Daraja expects automatically — your handler
only needs to react to the payload.
The same processing logic is available for MVC controllers via DarajaWebhookHandler (e.g.
DarajaWebhookHandler.ProcessResultCallbackAsync(Request, onResult) from a controller action)
if you'd rather not use Minimal API routing.
CLI tool
dotnet tool install --global dotnet-safaricom-daraja
# Interactive setup wizard: configure credentials, pick a host style, scaffold sample code
safaricom-daraja init
# Non-interactive
safaricom-daraja init --consumer-key YOUR_KEY --consumer-secret YOUR_SECRET \
--environment Sandbox --shortcode 174379 --passkey YOUR_PASSKEY --framework minimal -y
# Verify your credentials work against Daraja
safaricom-daraja token
# Raise an STK Push, then check its outcome
safaricom-daraja stk --shortcode 174379 --passkey YOUR_PASSKEY --phone 254712345678 \
--amount 1 --callback-url https://example.com/mpesa/stk-callback
safaricom-daraja stk --shortcode 174379 --passkey YOUR_PASSKEY --query ws_CO_...
# C2B: register callback URLs, or (Sandbox only) simulate an incoming payment
safaricom-daraja c2b --shortcode 600000 --confirmation-url https://example.com/payments/c2b/confirmation \
--validation-url https://example.com/payments/c2b/validation
safaricom-daraja c2b --simulate --shortcode 600000 --phone 254712345678 --amount 100 --bill-ref INV-0001
# B2C / B2B / Reversal / Transaction Status / Account Balance all need a SecurityCredential
safaricom-daraja b2c --initiator apiuser --shortcode 600000 --phone 254712345678 --amount 500 \
--remarks Refund --result-url https://example.com/mpesa/b2c/result --timeout-url https://example.com/mpesa/b2c/timeout \
--cert path/to/cert.cer --initiator-password YourInitiatorPassword
# Dynamic QR Code
safaricom-daraja qr --merchant-name "Example Store" --reference-no INV-0001 --amount 500 --cpi 174379 --output qr.png
# M-Pesa Ratiba standing order
safaricom-daraja ratiba --name "Monthly subscription" --shortcode 174379 --phone 254712345678 --amount 500 \
--callback-url https://example.com/mpesa/ratiba-callback --account-reference SUB-001 \
--start-date 2026-10-01 --end-date 2027-10-01
# Bill Manager
safaricom-daraja bill-manager --optin --shortcode 174379 --email billing@example.com \
--contact 254712345678 --callback-url https://example.com/mpesa/billmanager/reconciliation
# Offline cryptography self-check (SecurityCredential RSA round-trip, STK password format)
safaricom-daraja test
Every core Daraja service area (STK Push/Query, C2B, B2C, B2B + Tax Remittance, Reversal,
Transaction Status, Account Balance, Ratiba, Lipa na Bonga, Pull API, Dynamic QR, Bill Manager)
has a CLI command — run safaricom-daraja --help for the full flag reference for each. (The
Safaricom.Daraja.IoT module isn't wired into the CLI — it's a separate package with a
different auth model.)
init detects whether you're in an ASP.NET Core Minimal API, MVC, or plain console project and
scaffolds a working STK Push example (endpoint/controller + callback handler) plus an
appsettings.json "Daraja" section, wired for AddDaraja(builder.Configuration).
Environment switching
var config = new DarajaConfig
{
Environment = DarajaEnvironment.Sandbox, // default — fails safe against real money
// Environment = DarajaEnvironment.Production,
ConsumerKey = "...",
ConsumerSecret = "...",
};
DarajaConfig also reads DARAJA_CONSUMER_KEY, DARAJA_CONSUMER_SECRET,
DARAJA_ENVIRONMENT (Sandbox/Production), and DARAJA_BASE_ADDRESS from environment
variables as a fallback, so credentials never need to be hardcoded.
C2B.SimulateAsync throws if called against Production — Safaricom doesn't expose a
simulate endpoint there, since live payments arrive through real M-Pesa traffic.
Security note: the SecurityCredential certificate
B2C, B2B, Reversal, and Account Balance all require a SecurityCredential: your Initiator
password, RSA-encrypted with a Safaricom-issued public certificate. This SDK does not
bundle either certificate. Third-party copies circulating in various open-source repos
have in the past turned out to be the wrong certificate (silently producing credentials
Daraja rejects) — always download yours directly from the
Daraja developer portal (Docs → APIs → Authorization),
and note that Sandbox and Production use different certificates.
var securityCredential = SecurityCredentialEncryptor.EncryptFromCertificateFile(
"path/to/your-downloaded-cert.cer",
"YourInitiatorPassword");
A known Daraja quirk this SDK handles for you
- C2B
RegisterUrltypo: Daraja's own response ships the field misspelled asOriginatorCoversationID.C2BRegisterUrlResponse.OriginatorConversationIdmaps to that exact wire spelling so you don't have to think about it. - Bill Manager's inconsistent envelope: unlike the rest of Daraja
(
ResponseCode/ResponseDescription), Bill Manager responses userescode/resmsg.BillManagerResponsemaps both. - OAuth
expires_inis a string, not a JSON number.DarajaTokenparses it defensively. - C2B
RegisterUrlrejects any callback URL containing the word "mpesa" — confirmed live against the Sandbox:ValidationURL/ConfirmationURLvalues likehttps://example.com/mpesa/c2b/validationfail with a 400 (Bad Request - Invalid ValidationURL - URL has the word MPESA). This SDK can't work around it for you since it's an outbound URL you choose — just avoid the word in that specific path; every example in this repo uses/payments/...instead.
Verified against the live Sandbox
The core client, OAuth flow, and STK Push were run against Safaricom's real Sandbox during
development (not just unit-tested against mocks) using the samples/Safaricom.Daraja.Sample.Console
project:
- ✅ OAuth token acquisition — real tokens issued and cached correctly, across multiple runs.
- ✅ STK Push + Query — real
CheckoutRequestIDs returned and successfully queried (result codes varied run to run as expected:4999"still processing",1037"user cannot be reached" — both are Daraja telling you nobody entered a PIN on the Sandbox test number, not SDK errors). - ✅ C2B Simulate — accepted successfully.
- ⚠️ C2B
RegisterUrl— the request shape is confirmed correct (Daraja validated and rejected the URL content specifically, proving the payload parsed correctly), but registration then hit a500 Service is currently unreachableon retry. Shortcode174379is Safaricom's shared public Sandbox test shortcode used by every Daraja developer worldwide, so this looks like registration contention on that shortcode rather than an SDK issue — try your own dedicated Sandbox shortcode if you hit the same thing. - ⚠️ Dynamic QR Code — consistently returned an HTTP 503 with an infrastructure-level "Application is not available" HTML page (not a Daraja JSON error), suggesting that specific Sandbox microservice was down during testing rather than a request-shape problem. Untested beyond that.
Safaricom.Daraja.IoT: SIM-portal & IMSI
This is a separate Safaricom product from core Daraja — it shares the same Sandbox/Production hosts, but a different auth/header scheme, and it isn't part of Safaricom's public Daraja documentation. It's bundled here because it shipped in the same source Postman collection this whole SDK was built from.
using Safaricom.Daraja.IoT;
using Safaricom.Daraja.IoT.Models;
var iot = new IoTClient(new IoTConfig
{
ApiKey = "YOUR_X_API_KEY",
Msisdn = "254712345678", // your operating system's identifying MSISDN
// Either supply a pre-acquired token directly...
AccessToken = "YOUR_BEARER_TOKEN",
// ...or let the SDK fetch/cache one via /oauth/v1/generate, same as core Daraja
// (unconfirmed whether this product accepts that flow for your app registration - try it):
// ConsumerKey = "...", ConsumerSecret = "...",
});
var result = await iot.Messaging.SendSingleMessageAsync(new SendSingleMessageRequest
{
Msisdn = "254712345678",
Message = "Your SIM has been activated.",
VpnGroup = "your-vpn-group",
Username = "your-portal-username",
});
Console.WriteLine(result.Json); // raw response - see note below
Why raw JSON, not typed models? Every response in this module comes back as
DarajaRawResult.Json rather than a typed C# object. Unlike core Daraja, this product has no
public documentation this SDK could verify response shapes against — presenting a guessed typed
model here would risk silently misleading you about field names that don't actually exist.
Request bodies, by contrast, are fully typed: they're taken directly from the reference Postman
collection's request payloads, which are unambiguous.
What's confirmed vs. not: the endpoints, HTTP methods, and request bodies below are taken
directly from the reference collection. The exact semantics of the X-MessageID header
(pass via IoTCallOptions.MessageId) are not — it resembles a conversation-continuation token
in the source collection, but this SDK cannot confirm whether it's required, generated, or
echoed back from a prior response. Leave it unset unless your integration has been told otherwise.
| Client | Covers |
|---|---|
iot.Messaging |
Search/filter/get-all messages, send a single message, delete a message or thread |
iot.SimOperations |
List SIMs, lifecycle status, customer info, activation (+ trends), rename asset, location info, suspend/unsuspend |
iot.Imsi |
CheckATI v1, and "SWAP CheckATI" (v2) |
Lipa na Bonga: response shape is unconfirmed
Lipa na Bonga isn't part of Safaricom's public Daraja catalog, so unlike every other area in
this SDK, its response schema hasn't been cross-checked against official documentation.
LipaNaBongaClient returns the raw response body (DarajaRawResult.Json) rather than a
typed model with guessed field names. If you've confirmed the real shape against your own
sandbox, contributions adding a typed model are welcome.
Live-tested finding: safaricom-daraja bonga --calculate --points 100 against the real
Sandbox with a standard app registration returned 404 Not Found on
/v1/lipa/na/bonga/calculator-points. The request shape matches the reference collection
exactly, so this looks like the product needing separate account enablement rather than a bug
in this SDK — but if you get it working, we'd like to know what's different about your setup.
Examples & Samples
examples/— four self-contained, copy-pasteable demos:console-script,aspnetcore-minimal-api,aspnetcore-mvc,worker-service. Each references the published NuGet packages directly — seeexamples/README.md.samples/Safaricom.Daraja.Sample.Console— part of the main solution (uses a localProjectReference); this is the project used for the live Sandbox verification above.
Roadmap
- Cross-check
Safaricom.Daraja.IoT(SIM-portal/IMSI) and the less common core areas (Ratiba, Bill Manager, Lipa na Bonga, Pull API) against real Sandbox credentials — only OAuth, STK Push/Query, and C2B Simulate have been run against the live Sandbox so far. - Add a NuGet package icon.
Sponsorship
If this SDK helps you in your projects or commercial integrations, consider supporting ongoing open-source maintenance and development:
👉 Click here to Support via Pesapal Open Source Sponsorship
License
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net5.0 was computed. net5.0-windows was computed. net6.0 was computed. net6.0-android was computed. net6.0-ios was computed. net6.0-maccatalyst was computed. net6.0-macos was computed. net6.0-tvos was computed. net6.0-windows was computed. net7.0 was computed. net7.0-android was computed. net7.0-ios was computed. net7.0-maccatalyst was computed. net7.0-macos was computed. net7.0-tvos was computed. net7.0-windows was computed. net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
| .NET Core | netcoreapp2.0 was computed. netcoreapp2.1 was computed. netcoreapp2.2 was computed. netcoreapp3.0 was computed. netcoreapp3.1 was computed. |
| .NET Standard | netstandard2.0 is compatible. netstandard2.1 was computed. |
| .NET Framework | net461 was computed. net462 was computed. net463 was computed. net47 was computed. net471 was computed. net472 was computed. net48 was computed. net481 was computed. |
| MonoAndroid | monoandroid was computed. |
| MonoMac | monomac was computed. |
| MonoTouch | monotouch was computed. |
| Tizen | tizen40 was computed. tizen60 was computed. |
| Xamarin.iOS | xamarinios was computed. |
| Xamarin.Mac | xamarinmac was computed. |
| Xamarin.TVOS | xamarintvos was computed. |
| Xamarin.WatchOS | xamarinwatchos was computed. |
-
.NETStandard 2.0
- System.Text.Json (>= 8.0.5)
-
net10.0
- No dependencies.
-
net8.0
- No dependencies.
NuGet packages (2)
Showing the top 2 NuGet packages that depend on Safaricom.Daraja:
| Package | Downloads |
|---|---|
|
Safaricom.Daraja.IoT
.NET client for Safaricom's IoT SIM-portal (SIM messaging and subscriber/SIM operations) and IMSI CheckATI APIs. A distinct product from core Daraja with its own auth/header scheme and no public documentation - responses are returned as raw JSON since this SDK cannot verify a typed response shape against official docs. |
|
|
Safaricom.Daraja.AspNetCore
ASP.NET Core integration for the Safaricom Daraja API. Dependency injection extensions, Minimal API webhook endpoint mapping (STK Push, C2B, B2C/B2B/Reversal/Transaction Status/Account Balance result callbacks, Bill Manager reconciliation), and MVC webhook processing helpers. |
GitHub repositories
This package is not used by any popular GitHub repositories.
0.2.0: Version alignment with the CLI's expanded command coverage - no functional changes to the core client since 0.1.0. 0.1.0: Initial preview release of the core Daraja client (OAuth, STK Push, C2B, B2C, B2B, Reversal, Transaction Status, Account Balance, Ratiba, Lipa na Bonga, Pull API, Dynamic QR, Bill Manager, Tax Remittance).