API Reference

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).

Quickstart

  1. Sign up and confirm your email.
  2. Add a default payout account (M-Pesa phone, Paybill, Till, or Bank).
  3. Generate an API key in Dashboard → API keys. Test keys start with mpg_test_.
  4. Call POST /api/v1/stk-push with the customer phone and amount.
  5. Either poll GET /api/v1/transactions/:id or register a webhook URL.

Authentication

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

Initiate STK Push

POST/api/v1/stk-push

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.

Headers

AuthorizationrequiredheaderBearer mpg_test_... or mpg_live_...
Content-Typerequiredheaderapplication/json
Idempotency-KeyheaderOptional. Unique per logical request. Replays within 24h return the existing transaction instead of creating a duplicate.

Request body

amountrequirednumberAmount in KES, max 150,000.
phonerequiredstringCustomer phone. Accepts 2547XXXXXXXX, 07XXXXXXXX, or 01XXXXXXXX.
referencerequiredstringYour internal reference (max 40 chars).
descriptionrequiredstringShort description (max 60 chars) shown to the customer.

Success response (200)

{
  "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…"
}

Get Transaction Status

GET/api/v1/transactions/:id

Poll this endpoint while waiting for the customer to respond to the STK prompt. Status transitions are pendingsuccess | 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"
  }
}

Webhooks

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.successeventCustomer paid. Includes mpesa_receipt.
payment.failedeventCustomer cancelled, timed out, or insufficient funds.
payout.successeventB2C payout to your M-Pesa phone completed.
payout.failedeventB2C payout failed. Check result_desc.
payout.pending_manualeventFunds received but your payout account is Paybill/Till/Bank — queued for manual settlement.

Verifying signatures (Node.js)

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();
}

Errors

All error responses are JSON of the shape { "error": "...", "hint": "...", "detail": "..." }.

400statusValidation failed, invalid phone, no payout account, or test/live mode mismatch.
401statusMissing, invalid, or revoked API key.
404statusTransaction not found (or not owned by your account).
500statusServer misconfiguration (e.g. PUBLIC_SITE_URL not set) or internal error.
502statusDaraja STK Push call failed (credentials, shortcode, or upstream error).

Sandbox vs Live

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.

Code samples

curl

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"
  }'

Node.js (fetch)

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);

Python (requests)

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 (cURL)

<?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.