hassle-free M-Pesa integration

mcp.paylod.dev · npx @paylod/cli · 23 tools

M-Pesa for AI agents, over MCP and CLI.

Let an AI agent build your M-Pesa integration — or let your agentic system charge M-Pesa itself, at runtime.

  • get_callback_urlhosted · HTTPS · yours
  • mint_keymp_test_9737baac… (shown once)
  • get_payment_statuspaid · UGEBCBMH8G

No callback server to host.

You get one signed, idempotent webhook.

M-Pesapaylod callbackyour endpoint

No per-transaction cut.

100%

of every KES you collect stays yours.

No custody.

Your paybill, your till, your Daraja credentials. M-Pesa settles straight to you.

// 1 — OAuth. Daraja tokens die hourly; you cache them yourself.
let cached = { token: null, exp: 0 };
async function darajaToken() {
  if (cached.token && Date.now() < cached.exp) return cached.token;
  const basic = Buffer.from(`${KEY}:${SECRET}`).toString("base64");
  const r = await fetch(
    "https://api.safaricom.co.ke/oauth/v1/generate?grant_type=client_credentials",
    { headers: { Authorization: `Basic ${basic}` } },
  );
  const j = await r.json();
  cached = { token: j.access_token, exp: Date.now() + (j.expires_in - 60) * 1000 };
  return cached.token;
}

// 2 — Password: base64(shortcode + passkey + timestamp), Africa/Nairobi.
const ts = format(new Date(), "yyyyMMddHHmmss");
const password = Buffer.from(SHORTCODE + PASSKEY + ts).toString("base64");

// 3 — The STK Push payload.
const res = await fetch(`${BASE}/mpesa/stkpush/v1/processrequest`, {
  method: "POST",
  headers: { Authorization: `Bearer ${await darajaToken()}` },
  body: JSON.stringify({
    BusinessShortCode: SHORTCODE,
    Password: password,
    Timestamp: ts,
    TransactionType: "CustomerPayBillOnline",
    Amount: 100,
    PartyA: "254712345678",
    PartyB: SHORTCODE,
    PhoneNumber: "254712345678",
    CallBackURL: "https://a1b2c3.ngrok.io/mpesa/callback",
    AccountReference: "order_1",
    TransactionDesc: "order_1",
  }),
});

// 4 — Host that callback. Public HTTPS, unauthenticated, forever.
app.post("/mpesa/callback", async (req, res) => {
  res.json({ ResultCode: 0, ResultDesc: "Accepted" });
  const cb = req.body.Body.stkCallback;

  // 5 — Decode the result code: 1 → no funds · 1032 → cancelled · 2001 → wrong PIN
  if (cb.ResultCode !== 0) return markFailed(cb.CheckoutRequestID, cb.ResultCode);

  // 6 — Dig the receipt out of a nested, unordered Item array.
  const items = cb.CallbackMetadata.Item;
  const get = (n) => items.find((i) => i.Name === n)?.Value;
  await markPaid(cb.CheckoutRequestID, {
    receipt: get("MpesaReceiptNumber"),
    amount: get("Amount"),
    phone: String(get("PhoneNumber")),
  });

  // 7 — Then: idempotency, retries, reconciliation, ngrok on your laptop.
});
import { Paylod } from "@paylod/node";

const paylod = new Paylod();

// Rings the phone, waits for the PIN,
// decodes the result. There is no step 2.
const r = await paylod.collectAndWait({
  amount: 100,
  phone: "0712345678",
});

if (r.ok) fulfil(r.receipt);
else notify(r.error.customerMessage);
before paylodafter paylod