Kenya Escrow Business API v1 — add M-Pesa escrow to your platform.
1. Register → verify your email → ake_test_key is issued instantly 2. Create test transactions and simulate payments — no real money moves 3. Submit business details under Compliance → approved →ke_live_key 4. Live transactions trigger a real M-Pesa STK push to the buyer's phone
Every request needs a Bearer key. Test keys create sandbox transactions only; live keys create real ones.
Authorization: Bearer ke_test_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
POST /api/v1/transactions Idempotency-Key: order-1001 # recommended — retries never create duplicates { "seller_amount": 5000, // KSh the seller receives (50 – 240,000) "seller_phone": "2547XXXXXXXX", // seller's M-Pesa number "buyer_phone": "2547XXXXXXXX", // buyer's M-Pesa number (gets the STK push) "item_name": "iPhone 14 Pro", "description": "256GB, sealed", "duration_hours": 48 // 0.5 – 720 }
// 200 OK { "ok": true, "data": { "id": "G79WBTSD", "status": "pending", // pending → held → completed | disputed | refunded | expired "seller_amount": 5000, "total": 5420, // buyer pays this (includes escrow fee) "buyer_link": "https://kenyaescrow.com/TMR-a1b2c3...", // secret — release/dispute controls "seller_link": "https://kenyaescrow.com/G79WBTSD", // view-only tracking "stk_push": "sent", // live keys only; "skipped_test" in sandbox "test": false } }
Send the buyer_link to your buyer (or embed it in your UI). The buyer sees the status page; the seller gets tracking via seller_link plus automatic SMS & WhatsApp alerts. Buyers/sellers never create accounts.
$ch = curl_init('https://kenyaescrow.com/api/v1/transactions');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . $apiKey,
'Content-Type: application/json',
'Idempotency-Key: order-1001',
],
CURLOPT_POSTFIELDS => json_encode([
'seller_amount' => 5000,
'seller_phone' => '2547XXXXXXXX',
'buyer_phone' => '2547XXXXXXXX',
'item_name' => 'iPhone 14 Pro',
'duration_hours' => 48,
]),
]);
$tx = json_decode(curl_exec($ch), true)['data'];
// $tx['buyer_link'] → send to your buyerconst res = await fetch('https://kenyaescrow.com/api/v1/transactions', {
method: 'POST',
headers: {
'Authorization': `Bearer ${apiKey}`,
'Content-Type': 'application/json',
'Idempotency-Key': 'order-1001',
},
body: JSON.stringify({
seller_amount: 5000,
seller_phone: '2547XXXXXXXX',
buyer_phone: '2547XXXXXXXX',
item_name: 'iPhone 14 Pro',
duration_hours: 48,
}),
});
const { data: tx } = await res.json();
// tx.buyer_link → send to your buyerimport requests
res = requests.post(
'https://kenyaescrow.com/api/v1/transactions',
headers={
'Authorization': f'Bearer {api_key}',
'Idempotency-Key': 'order-1001',
},
json={
'seller_amount': 5000,
'seller_phone': '2547XXXXXXXX',
'buyer_phone': '2547XXXXXXXX',
'item_name': 'iPhone 14 Pro',
'duration_hours': 48,
},
)
tx = res.json()['data']
# tx['buyer_link'] → send to your buyerHttpClient client = HttpClient.newHttpClient();
HttpRequest req = HttpRequest.newBuilder()
.uri(URI.create("https://kenyaescrow.com/api/v1/transactions"))
.header("Authorization", "Bearer " + apiKey)
.header("Content-Type", "application/json")
.header("Idempotency-Key", "order-1001")
.POST(HttpRequest.BodyPublishers.ofString("""
{"seller_amount":5000,"seller_phone":"2547XXXXXXXX",
"buyer_phone":"2547XXXXXXXX","item_name":"iPhone 14 Pro",
"duration_hours":48}"""))
.build();
String json = client.send(req, HttpResponse.BodyHandlers.ofString()).body();
// parse json → data.buyer_link → send to your buyercurl -X POST https://kenyaescrow.com/api/v1/transactions \
-H "Authorization: Bearer ke_test_xxxx" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: order-1001" \
-d '{"seller_amount":5000,"seller_phone":"2547XXXXXXXX",
"buyer_phone":"2547XXXXXXXX","item_name":"iPhone 14 Pro",
"duration_hours":48}'Always send a unique Idempotency-Key per order (e.g. your internal order ID). If a retry arrives with the same key, we return the original transaction — a second order is never created. Replays carry the X-Idempotent-Replay: true header. Keys are scoped to your account and never expire. If you don't send one, identical request bodies within a ~5-minute window are still deduplicated automatically.
GET /api/v1/transactions/{id} // single
GET /api/v1/transactions?page=1&per_page=20&status=Pending // your transactions
pending | Created, awaiting buyer payment |
held | Buyer paid — funds held in escrow |
completed | Buyer released — seller paid out via B2C |
disputed | Dispute raised — under admin review |
refunded | Returned to buyer |
expired | Deadline reached before release |
Flip a sandbox transaction to paid without touching M-Pesa — fires a real signed webhook so you can test your handler:
POST /api/v1/test/transactions/{id}/simulate-payment // ke_test_ keys only
Test transactions show a TEST MODE banner on their escrow page, get TEST- receipts, and skip SMS/WhatsApp/B2C.
Set your endpoint on the dashboard. Every status change POSTs signed JSON:
POST https://yoursite.co.ke/ke-webhook
X-KE-Event: transaction.paid
X-KE-Signature: t=1789846011,v1=a15796da…
{"event":"transaction.paid","transaction_id":"AP9XUEQJ","test":true,
"timestamp":1789846011,"data":{"status":"Pending","seller_amount":2000,
"total":2180,"buyer_link":"…","paid_at":"…"}}
$raw = file_get_contents('php://input');
[$ts, $sig] = explode(',', $_SERVER['HTTP_X_KE_SIGNATURE']);
$sig = substr($sig, 3); // strip "v1="
$ts = substr($ts, 2); // strip "t="
$calc = hash_hmac('sha256', "$ts.$raw", $secret);
if (!hash_equals($calc, $sig)) { http_response_code(401); exit; }
$event = json_decode($raw, true);import crypto from 'crypto';
// inside your POST handler — `rawBody` = raw request body string
const [tsPart, sigPart] = req.headers['x-ke-signature'].split(',');
const ts = tsPart.slice(2);
const sig = sigPart.slice(3);
const calc = crypto.createHmac('sha256', secret)
.update(`${ts}.${rawBody}`).digest('hex');
if (!crypto.timingSafeEqual(Buffer.from(calc), Buffer.from(sig))) {
return res.status(401).end();
}
const event = JSON.parse(rawBody);import hmac, hashlib
# inside your POST handler — raw_body = raw request body bytes
ts, sig = request.headers['X-KE-Signature'].split(',')
ts, sig = ts[2:], sig[3:]
calc = hmac.new(secret.encode(), f'{ts}.'.encode() + raw_body,
hashlib.sha256).hexdigest()
if not hmac.compare_digest(calc, sig):
return '', 401
event = json.loads(raw_body)// inside your POST handler — rawBody = raw request body string
String[] parts = request.getHeader("X-KE-Signature").split(",");
String ts = parts[0].substring(2), sig = parts[1].substring(3);
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(secret.getBytes(), "HmacSHA256"));
byte[] raw = mac.doFinal((ts + "." + rawBody).getBytes());
String calc = HexFormat.of().formatHex(raw);
if (!MessageDigest.isEqual(calc.getBytes(), sig.getBytes())) {
response.setStatus(401); return;
}Events: transaction.paid · transaction.released · transaction.disputed · transaction.refunded · transaction.expired
401 | Missing/invalid key |
403 | Wrong key type (e.g. live key on a test-only endpoint) |
422 | Validation — check error.code and error.message |
429 | Rate limit — 30/min test, 60/min live. Respect Retry-After |
Limits: seller receives KSh 50 – 240,000 per transaction; buyer total stays within the KSh 250,000 M-Pesa cap. Fees are identical to the public site.