OBED TECH GATEWAY exposes a small REST API to trigger M-Pesa STK Push, poll transaction status, and receive webhook notifications. Base URL is your deployment origin (e.g. https://your-app.vercel.app).
mpg_test_.POST /api/v1/stk-push with the customer phone and amount.GET /api/v1/transactions/:id or register a webhook URL.Every request requires a Bearer token. Test keys begin with mpg_test_; live keys with mpg_live_. Keys are shown only once at creation — store them in a secret manager.
Authorization: Bearer mpg_live_xxxxxxxxxxxxxxxx
Sends an STK prompt to the customer's phone. Funds settle to the platform shortcode; if your default payout account is an M-Pesa phone we automatically B2C the net amount to it. Paybill/Till/Bank payouts are queued for manual settlement and you receive a payout.pending_manual webhook.
Authorizationrequired | header | Bearer mpg_test_... or mpg_live_... |
Content-Typerequired | header | application/json |
Idempotency-Key | header | Optional. Unique per logical request. Replays within 24h return the existing transaction instead of creating a duplicate. |
amountrequired | number | Amount in KES, max 150,000. |
phonerequired | string | Customer phone. Accepts 2547XXXXXXXX, 07XXXXXXXX, or 01XXXXXXXX. |
referencerequired | string | Your internal reference (max 40 chars). |
descriptionrequired | string | Short description (max 60 chars) shown to the customer. |
{
"success": true,
"transaction_id": "8a4b…",
"merchant_request_id": "29115-34620561-1",
"checkout_request_id": "ws_CO_27042026…",
"customer_message": "Success. Request accepted for processing",
"amount": 100,
"platform_fee": 1.5,
"net_amount": 98.5,
"status": "pending",
"poll_url": "/api/v1/transactions/8a4b…"
}Poll this endpoint while waiting for the customer to respond to the STK prompt. Status transitions are pending → success | failed.
{
"transaction": {
"id": "8a4b…",
"reference": "ORDER-1234",
"amount": 100,
"platform_fee": 1.5,
"net_amount": 98.5,
"status": "success",
"mpesa_receipt_number": "SGH7XYZABC",
"result_code": "0",
"result_desc": "The service request is processed successfully.",
"completed_at": "2026-04-27T12:34:56Z"
}
}Add a webhook URL in Dashboard → Webhooks. We POST a JSON body with these events. Each request includes X-Mpesa-Gateway-Signature (HMAC-SHA256 of the raw body using your endpoint secret) and X-Mpesa-Gateway-Event. Failed deliveries are retried up to 3 times with exponential backoff.
payment.success | event | Customer paid. Includes mpesa_receipt. |
payment.failed | event | Customer cancelled, timed out, or insufficient funds. |
payout.success | event | B2C payout to your M-Pesa phone completed. |
payout.failed | event | B2C payout failed. Check result_desc. |
payout.pending_manual | event | Funds received but your payout account is Paybill/Till/Bank — queued for manual settlement. |
import crypto from "crypto";
const expected = crypto
.createHmac("sha256", endpointSecret)
.update(rawBody) // the raw request body, not parsed JSON
.digest("hex");
if (expected !== req.headers["x-mpesa-gateway-signature"]) {
return res.status(401).end();
}All error responses are JSON of the shape { "error": "...", "hint": "...", "detail": "..." }.
400 | status | Validation failed, invalid phone, no payout account, or test/live mode mismatch. |
401 | status | Missing, invalid, or revoked API key. |
404 | status | Transaction not found (or not owned by your account). |
500 | status | Server misconfiguration (e.g. PUBLIC_SITE_URL not set) or internal error. |
502 | status | Daraja STK Push call failed (credentials, shortcode, or upstream error). |
Test keys (mpg_test_) only work when the gateway is running against Daraja sandbox. Live keys (mpg_live_) require Daraja production credentials. Use Safaricom's sandbox phone 254708374149 for end-to-end testing — it always accepts the prompt.
curl -X POST https://your-app.vercel.app/api/v1/stk-push \
-H "Authorization: Bearer mpg_test_xxxxxxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: order-1234-attempt-1" \
-d '{
"amount": 100,
"phone": "254712345678",
"reference": "ORDER-1234",
"description": "T-shirt"
}'const res = await fetch("https://your-app.vercel.app/api/v1/stk-push", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.MPESA_GATEWAY_KEY}`,
"Content-Type": "application/json",
"Idempotency-Key": `order-${orderId}`,
},
body: JSON.stringify({
amount: 100,
phone: "254712345678",
reference: `ORDER-${orderId}`,
description: "T-shirt",
}),
});
const data = await res.json();
console.log(data.transaction_id);import os, requests
r = requests.post(
"https://your-app.vercel.app/api/v1/stk-push",
headers={
"Authorization": f"Bearer {os.environ['MPESA_GATEWAY_KEY']}",
"Content-Type": "application/json",
"Idempotency-Key": f"order-{order_id}",
},
json={
"amount": 100,
"phone": "254712345678",
"reference": f"ORDER-{order_id}",
"description": "T-shirt",
},
timeout=30,
)
r.raise_for_status()
print(r.json()["transaction_id"])<?php
$ch = curl_init("https://your-app.vercel.app/api/v1/stk-push");
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
"Authorization: Bearer " . getenv("MPESA_GATEWAY_KEY"),
"Content-Type: application/json",
"Idempotency-Key: order-" . $orderId,
],
CURLOPT_POSTFIELDS => json_encode([
"amount" => 100,
"phone" => "254712345678",
"reference" => "ORDER-" . $orderId,
"description" => "T-shirt",
]),
]);
$response = curl_exec($ch);
curl_close($ch);
echo $response;Need help? Check the dashboard for live transaction logs and webhook delivery attempts.