C++ integration guide
A minimal libcurl + nlohmann/json client for registering, storing credentials and charging with M-Pesa.
Prerequisites
- A C++17 compiler and CMake
libcurlandnlohmann-json(via vcpkg, Conan, or your package manager)
find_package(CURL REQUIRED)
find_package(nlohmann_json REQUIRED)
add_executable(gateway_client main.cpp)
target_link_libraries(gateway_client PRIVATE CURL::libcurl nlohmann_json::nlohmann_json)
1. Build a gateway client
A small wrapper around libcurl keeps every call to the gateway consistent — one place to attach the X-API-Key header and parse JSON.
#include <curl/curl.h>
#include <nlohmann/json.hpp>
#include <string>
using json = nlohmann::json;
class GatewayClient {
public:
explicit GatewayClient(std::string apiKey = "")
: baseUrl_("https://payments.navipos.co.ke"), apiKey_(std::move(apiKey)) {}
json post(const std::string& path, const json& body) {
return request("POST", path, &body);
}
json get(const std::string& path) {
return request("GET", path, nullptr);
}
private:
static size_t writeCallback(char* ptr, size_t size, size_t nmemb, void* userdata) {
static_cast<std::string*>(userdata)->append(ptr, size * nmemb);
return size * nmemb;
}
json request(const std::string& method, const std::string& path, const json* body) {
CURL* curl = curl_easy_init();
std::string responseBody;
std::string url = baseUrl_ + path;
struct curl_slist* headers = nullptr;
headers = curl_slist_append(headers, "Content-Type: application/json");
if (!apiKey_.empty()) {
headers = curl_slist_append(headers, ("X-API-Key: " + apiKey_).c_str());
}
curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writeCallback);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &responseBody);
std::string payload;
if (body) {
payload = body->dump();
curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, method.c_str());
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, payload.c_str());
} else if (method == "POST") {
curl_easy_setopt(curl, CURLOPT_POST, 1L);
}
curl_easy_perform(curl);
curl_slist_free_all(headers);
curl_easy_cleanup(curl);
return responseBody.empty() ? json::object() : json::parse(responseBody);
}
std::string baseUrl_;
std::string apiKey_;
};
2. Register a client
Create an account. It's created inactive and a 6-digit OTP is sent by SMS and email — registering does not return an API key.
GatewayClient client;
json payload = {
{"name", "My Business Ltd"},
{"businessType", "Retail"},
{"email", "owner@mybusiness.com"},
{"phone", "254712345678"},
{"password", "S3cure!pass"},
};
json result = client.post("/api/v1/clients", payload);
// Response: {"otp_required": true, "data": {"email": ..., "phone": ...}}
// No API key yet — the account is "inactive" and a 6-digit OTP has been sent.
3. Verify the OTP
This is the only step that returns your API key and client ID — save them now. The account moves to pending afterwards, awaiting admin approval before it can process live payments.
json verifyPayload = {
{"email", "owner@mybusiness.com"},
{"otp", "482913"},
};
json verified = client.post("/api/v1/auth/verify-otp", verifyPayload);
std::string apiKey = verified["data"]["apiKey"];
std::string clientId = verified["data"]["client"]["clientId"];
// This is the only response that includes apiKey/clientId — save them now.
4. Store provider credentials
Provider secrets are encrypted at rest — the same shape works for Daraja, KCB and Equity by changing provider. consumerKey/consumerSecret are always required; shortCode, passkey and initiatorPassword are required unless provider is kcb.
GatewayClient authed(apiKey);
json credentials = {
{"provider", "daraja"},
{"consumerKey", "your_consumer_key"},
{"consumerSecret", "your_consumer_secret"},
{"shortCode", "174379"},
{"passkey", "your_passkey"},
{"initiatorPassword", "your_initiator_password"},
{"environment", "sandbox"},
};
authed.post("/api/v1/clients/" + clientId + "/credentials", credentials);
5. Trigger an STK push
Only phoneNumber and amount are required — reference and description get server-generated defaults if omitted.
// Only phoneNumber and amount are required.
json stkPayload = {
{"phoneNumber", "254712345678"},
{"amount", 1000},
{"reference", "ORDER-1042"},
{"description", "Payment for order #1042"},
};
json push = authed.post("/api/v1/stk-push", stkPayload);
std::string transactionId = push["data"]["transactionId"];
6. Confirm the payment
Poll the transaction until it resolves. For a production service, prefer a worker/queue over blocking a request thread.
#include <chrono>
#include <thread>
std::string pollTransaction(GatewayClient& client, const std::string& transactionId,
int maxAttempts = 10) {
for (int i = 0; i < maxAttempts; ++i) {
json result = client.get("/api/v1/transactions/" + transactionId);
std::string status = result["data"]["status"];
// Status values are lowercase: pending, initiated, completed, failed, cancelled.
if (status == "completed" || status == "failed" || status == "cancelled") {
return status;
}
std::this_thread::sleep_for(std::chrono::seconds(2));
}
throw std::runtime_error("Transaction status could not be determined in time");
}
curl_global_init once at program start if you use the client from multiple threads.Response codes
| 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. |