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_url→hosted · HTTPS · yours
- mint_key→mp_test_9737baac… (shown once)
- get_payment_status→paid · UGEBCBMH8G
No callback server to host.
You get one signed, idempotent webhook.
M-Pesa→paylod callback→your 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);