Guides / C++

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
  • libcurl and nlohmann-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.

gateway_client.hpp
#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.

register.cpp
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.

verify_otp.cpp
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.

credentials.cpp
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.

stk_push.cpp
// 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.

poll_transaction.cpp
#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");
}
libcurl is not thread-safe by default for global state — call curl_global_init once at program start if you use the client from multiple threads.

Response codes

CodeMeaningWhat 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.
Full request/response schema for every endpoint: API reference.