.NET / C# integration guide
Register a client, store your provider credentials and accept your first M-Pesa payment from a .NET 6+ application.
Prerequisites
This guide assumes:
- .NET 6.0 or later
- A working M-Pesa Daraja, KCB Buni or Equity Jenga developer account for your provider credentials
- An HTTPS endpoint you control, for the C2B confirmation step
Every request and response body on the gateway is JSON — no SDK is required, but the snippets below use System.Net.Http.Json to keep the code short.
1. Register a client
Create an account, then verify the OTP to receive your API key. The account is created inactive; a 6-digit OTP is sent by SMS and email. Verifying it moves the account to pending, awaiting admin approval before it can process live payments.
using System.Net.Http;
using System.Net.Http.Json;
public class GatewayClient
{
private readonly HttpClient _http;
private const string BaseUrl = "https://payments.navipos.co.ke";
public GatewayClient(string? apiKey = null)
{
_http = new HttpClient { BaseAddress = new Uri(BaseUrl) };
if (apiKey is not null)
{
_http.DefaultRequestHeaders.Add("X-API-Key", apiKey);
}
}
public HttpClient Http => _http;
}
public record RegisterRequest(
string Name, string BusinessType, string Email, string Phone, string Password);
public async Task<JsonDocument> RegisterClientAsync(GatewayClient client, RegisterRequest req)
{
var response = await client.Http.PostAsJsonAsync("/api/v1/clients", req);
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<JsonDocument>()
?? throw new InvalidOperationException("Empty response");
}
// Usage
var client = new GatewayClient();
await RegisterClientAsync(client, new RegisterRequest(
"My Business Ltd", "Retail", "owner@mybusiness.com", "254712345678", "S3cure!pass"));
// Response is {"otp_required":true,"data":{"email":..., "phone":...}} — no key yet.
// The account is created "inactive"; a 6-digit OTP is sent by SMS and email.
Once the OTP arrives, verify it to receive your API key and client ID — this is the only step that returns them:
public record VerifyOtpRequest(string Email, string Otp);
public async Task<(string Token, string ApiKey, string ClientId)> VerifyOtpAsync(
GatewayClient client, VerifyOtpRequest req)
{
var response = await client.Http.PostAsJsonAsync("/api/v1/auth/verify-otp", req);
response.EnsureSuccessStatusCode();
var doc = await response.Content.ReadFromJsonAsync<JsonDocument>();
var data = doc!.RootElement.GetProperty("data");
var token = data.GetProperty("token").GetString()!;
var apiKey = data.GetProperty("apiKey").GetString()!;
var clientId = data.GetProperty("client").GetProperty("clientId").GetString()!;
return (token, apiKey, clientId);
}
// This is the only place clientId and apiKey are returned — save them now.
// The account moves to "pending" after this and awaits admin approval
// before it can process live payments.
2. Store provider credentials
Attach your Daraja, KCB Buni or Equity Jenga secrets to the client. They're encrypted at rest — the API only ever returns them masked afterwards.
public record DarajaCredentials(
string Provider, string ConsumerKey, string ConsumerSecret,
string ShortCode, string Passkey, string InitiatorPassword, string Environment);
public async Task StoreCredentialsAsync(GatewayClient client, string clientId, DarajaCredentials creds)
{
var response = await client.Http.PostAsJsonAsync(
$"/api/v1/clients/{clientId}/credentials", creds);
response.EnsureSuccessStatusCode();
}
// Usage — clientId/apiKey came from VerifyOtpAsync above
var authed = new GatewayClient(apiKey);
await StoreCredentialsAsync(authed, clientId, new DarajaCredentials(
"daraja", "your_consumer_key", "your_consumer_secret",
"174379", "your_passkey", "your_initiator_password", "sandbox"));
// consumerKey/consumerSecret are always required; shortCode, passkey and
// initiatorPassword are required for daraja/equity (not for kcb).
provider to daraja, kcb or equity and the gateway routes accordingly.3. Trigger an STK push
Send a payment prompt straight to the customer's phone:
public record StkPushRequest(
string PhoneNumber, decimal Amount, string? Reference = null, string? Description = null);
public async Task<JsonDocument> InitiateStkPushAsync(GatewayClient client, StkPushRequest req)
{
var response = await client.Http.PostAsJsonAsync("/api/v1/stk-push", req);
response.EnsureSuccessStatusCode();
return (await response.Content.ReadFromJsonAsync<JsonDocument>())!;
}
// Usage — only PhoneNumber and Amount are required; Reference/Description
// get server-generated defaults if you omit them.
var push = await InitiateStkPushAsync(authed, new StkPushRequest(
"254712345678", 1000m, "ORDER-1042", "Payment for order #1042"));
var data = push.RootElement.GetProperty("data");
var transactionId = data.GetProperty("transactionId").GetString();
4. Confirm the payment
STK results resolve asynchronously. Poll the transaction until it leaves pending — in production you can space this out with backoff or trigger it from a background job.
public async Task<string> PollTransactionAsync(
GatewayClient client, string transactionId, int maxAttempts = 10)
{
for (var i = 0; i < maxAttempts; i++)
{
var response = await client.Http.GetAsync($"/api/v1/transactions/{transactionId}");
response.EnsureSuccessStatusCode();
var doc = await response.Content.ReadFromJsonAsync<JsonDocument>();
var status = doc!.RootElement.GetProperty("data").GetProperty("status").GetString();
// Status values are lowercase: pending, initiated, completed, failed, cancelled.
if (status is "completed" or "failed" or "cancelled")
{
return status;
}
await Task.Delay(2000);
}
throw new TimeoutException("Transaction status could not be determined in time");
}
5. Receiving C2B (Pay Bill) payments
For Pay Bill / Till collections, register your own confirmation URL with POST /api/v1/c2b-register. Safaricom's confirmation payload is relayed to that URL as-is:
// ASP.NET Core controller — this is YOUR endpoint. Register its URL with
// POST /api/v1/c2b-register so Safaricom's confirmation is relayed to it.
[ApiController]
[Route("payments")]
public class MpesaConfirmationController : ControllerBase
{
[HttpPost("confirmation")]
public IActionResult Confirmation([FromBody] JsonElement payload)
{
var transId = payload.GetProperty("TransID").GetString();
var amount = payload.GetProperty("TransAmount").GetString();
var billReference = payload.GetProperty("BillRefNumber").GetString();
var payerPhone = payload.GetProperty("MSISDN").GetString();
// Persist / reconcile the payment against billReference here.
return Ok(new { ResultCode = 0, ResultDesc = "Accepted" });
}
}
Response codes
Every response follows the same envelope; errors carry an error object with a message.
| Code | Meaning | What to do |
|---|---|---|
200 |
OK | Request succeeded. |
201 |
Created | Resource created. |
400 |
Bad Request | Check the request payload. |
401 |
Unauthorized | Provide a valid API key or JWT. |
404 |
Not Found | The resource does not exist. |
429 |
Too Many Requests | Back off and retry — rate limit hit. |
500 |
Server Error | Retry, then contact support. |