One API for M-Pesa, cards and bank transfers.
Kipa sits on Paystack and Flutterwave rails and normalises them into a single set of objects: charges, refunds, checkout links and settlements. Test keys work immediately — no verification required for the sandbox.
Quickstart
Charge a customer over M-Pesa in three steps: install, authenticate, create a charge.
npm install @kipa/node
# or
pip install kipa# Test keys are prefixed kipa_test_, live keys kipa_live_
export KIPA_SECRET_KEY=kipa_test_5f3c...import Kipa from "@kipa/node";
const kipa = new Kipa(process.env.KIPA_SECRET_KEY);
const charge = await kipa.charges.create({
amount: 250000, // minor units — KES 2,500.00
currency: "KES",
channel: "mpesa",
customer: { phone: "+254712345678", email: "amina@example.com" },
reference: "order_1042", // your idempotency key
metadata: { order_id: "1042" },
});
console.log(charge.status); // "pending" → STK push sent to the handsetcurl https://api.kipa.co.ke/v1/charges \
-H "Authorization: Bearer $KIPA_SECRET_KEY" \
-H "Content-Type: application/json" \
-d '{"amount":250000,"currency":"KES","channel":"mpesa",
"customer":{"phone":"+254712345678"},"reference":"order_1042"}'The charge settles asynchronously. Don't poll — listen for the charge.succeeded webhook below.
Webhook verification
Always verify the signature over the raw request body before parsing JSON, and use a constant-time comparison. Kipa signs with HMAC SHA-256; if you receive directly from the processors, Paystack uses HMAC SHA-512 and Flutterwave a static hash header.
import { createHmac, timingSafeEqual } from "node:crypto";
export async function POST({ request }: { request: Request }) {
const raw = await request.text();
const signature = request.headers.get("kipa-signature") ?? "";
const expected = createHmac("sha256", process.env.KIPA_WEBHOOK_SECRET!)
.update(raw)
.digest("hex");
const a = Buffer.from(signature);
const b = Buffer.from(expected);
if (a.length !== b.length || !timingSafeEqual(a, b)) {
return new Response("Invalid signature", { status: 401 });
}
const event = JSON.parse(raw);
if (event.type === "charge.succeeded") {
await fulfilOrder(event.data.reference);
}
return new Response("ok"); // respond 2xx within 5s; we retry 8x otherwise
}const expected = createHmac("sha512", PAYSTACK_SECRET_KEY)
.update(rawBody)
.digest("hex");
if (expected !== request.headers.get("x-paystack-signature")) {
return new Response("Invalid signature", { status: 401 });
}const hash = request.headers.get("verif-hash");
if (!hash || hash !== process.env.FLUTTERWAVE_WEBHOOK_HASH) {
return new Response("Invalid signature", { status: 401 });
}import hmac, hashlib
@app.post("/webhooks/kipa")
def kipa_webhook():
raw = request.get_data()
expected = hmac.new(
os.environ["KIPA_WEBHOOK_SECRET"].encode(), raw, hashlib.sha256
).hexdigest()
if not hmac.compare_digest(expected, request.headers.get("Kipa-Signature", "")):
return "Invalid signature", 401
event = request.get_json()
return "ok", 200SDKs
Node.js
@kipa/node
npm install @kipa/node
Python
kipa
pip install kipa
PHP (Laravel)
kipa/kipa-php
composer require kipa/kipa-php
Go
github.com/kipa/kipa-go
go get github.com/kipa/kipa-go
API reference
Base URL https://api.kipa.co.ke · Bearer auth · amounts in minor units · JSON only.
| Method | Endpoint | Description |
|---|---|---|
| POST | /v1/charges | Create a charge. Routes to M-Pesa STK push, card or bank transfer. |
| GET | /v1/charges/:id | Retrieve a single charge and its timeline. |
| GET | /v1/charges | List charges, filter by status, channel or date. |
| POST | /v1/refunds | Refund a succeeded charge, full or partial. |
| POST | /v1/checkout_links | Create a hosted, shareable payment page. |
| GET | /v1/settlements | List payouts to your bank or paybill. |
| GET | /v1/events | Replay webhook events from the last 30 days. |
{
"id": "chg_2f81b0",
"reference": "order_1042",
"amount": 250000,
"currency": "KES",
"channel": "mpesa",
"status": "succeeded", // pending | succeeded | failed | refunded
"processor": "paystack", // routed rail
"customer": { "phone": "+254712345678", "email": "amina@example.com" },
"fees": 4500,
"created_at": "2026-08-08T09:12:44Z"
}Errors & testing
| insufficient_funds | Customer's M-Pesa wallet or card was declined for balance. |
| stk_timeout | Customer did not enter their PIN within 60 seconds. |
| invalid_signature | Webhook signature did not match — reject the delivery. |
| duplicate_reference | A charge with that reference already exists (idempotency). |
| rate_limited | Over 100 requests/second — retry with exponential backoff. |
In test mode, phone numbers ending 0000 always succeed and 1111 always time out.