@mzawadi-mini-app/mini-app-pay
v0.3.2
Published
M-Pesa PayBill payments for Alipay mini-app webviews. ES5-safe, mandatory paybill (v2).
Maintainers
Readme
Mini-App Pay
A tiny JavaScript library for processing M-Pesa payments — PayBill, Till
(Buy Goods), and Bonga PayBill (pay a paybill with Bonga points) — inside
Alipay mini-app webviews. It is generic and not tied to any single business:
the caller supplies the paybill or till number, so anyone who needs to take
payments from inside a mini app can use it. It wraps Alipay's AlipayJSBridge
payment API behind simple, promise-based functions and returns a structured
result for both success and failure.
Two builds: legacy vs v2
The library ships in two flavours that expose the same MiniAppPay.makePayment
global but differ in one safety rule:
| Build | File(s) | paybillNo | Use it for |
|-------|---------|-------------|------------|
| Legacy | payment-v1.js, mini-app-pay.iife.js | Optional, built-in default | Existing embeds already on the CDN — behavior is frozen. |
| v2 (strict) | mini-app-pay.v2.js | Mandatory | All new integrations, npm, and the future CDN package. |
⚠️ Never load a legacy build and v2 on the same page — they both define the
MiniAppPayglobal and whichever loads last wins.
Why v2 requires paybillNo: for a globally-reused library, a hardcoded
default paybill risks routing real payments to the wrong recipient. In v2 a
missing/blank paybillNo fails immediately and never calls AlipayJSBridge.
Features
- 🔒 Native Alipay payment integration (M-Pesa PayBill, Till / Buy Goods & Bonga, KES)
- 🎯 Built-in consent dialog before every payment
- 🛡️ Structured error handling with clear messages
- 📊 Transaction reference echoed back on every response
- 🧱 ES5-safe v2 build for old Alipay webview JS engines
Installation
npm (bundlers / Node)
npm install @mzawadi-mini-app/mini-app-pay// ESM
import { makePayment } from "@mzawadi-mini-app/mini-app-pay";
// CommonJS
const { makePayment } = require("@mzawadi-mini-app/mini-app-pay");TypeScript declarations are bundled — no @types package needed.
CDN / <script> (Alipay webview)
Load the Alipay Web View SDK first, then the payment library.
<!-- 1. Alipay Web View SDK (provides window.AlipayJSBridge) -->
<script type="text/javascript" src="https://appx/web-view.min.js"></script>
<!-- 2. Load the v2 (strict) library from a public npm CDN -->
<!-- 2a. Recommended: unpkg -->
<script src="https://unpkg.com/@mzawadi-mini-app/mini-app-pay"></script>
<!-- 2b. Or jsDelivr -->
<!-- <script src="https://cdn.jsdelivr.net/npm/@mzawadi-mini-app/mini-app-pay"></script> -->
<!-- Tip: pin a version for production, e.g. .../[email protected] -->The
cdn.cheche.co.kehost serves the legacy v1 build only. New integrations must load v2 from unpkg or jsDelivr as shown above.
The <script> build attaches the global MiniAppPay.makePayment.
Quick Start (v2)
const PAYBILL_NO = "YOUR_PAYBILL_NUMBER"; // the business/paybill number that receives the money
async function handlePayment() {
const result = await MiniAppPay.makePayment({
amount: 100, // replace with the actual amount (KES)
transactionReference: "TXN_" + Date.now(), // replace with your own unique reference
reason: "Game purchase", // replace with what the payment is for
paybillNo: PAYBILL_NO, // replace with your paybill number (MANDATORY in v2)
});
if (result.success) {
console.log("Payment successful!", result.data.transactionId);
// Optionally forward the result to your own backend for reconciliation.
await fetch("/your/endpoint", { // replace with your server endpoint
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
transactionId: result.data.transactionId, // Alipay transaction id
transactionReference: result.transactionReference,
amount: 100, // the amount you charged
}),
});
} else if (
result.message === "Payment cancelled by user!" || // consent dialog (step 1)
(result.data && result.data.error === 201) // Alipay sheet (step 2)
) {
console.log("User cancelled the payment."); // not a hard error
} else {
console.error("Payment failed:", result.message);
}
}Quick Start — Till / Buy Goods (v2)
Same flow as PayBill, but routes to an M-Pesa Till (Buy Goods) number via
MiniAppPay.makeTillPayment(...). A Till payment is identified only by the till
number — there is no billReference.
const TILL_NUMBER = "YOUR_TILL_NUMBER"; // the Till (Buy Goods) number that receives the money
async function handleTillPayment() {
const result = await MiniAppPay.makeTillPayment({
amount: 25, // replace with the actual amount (KES)
tillNumber: TILL_NUMBER, // replace with your till number (MANDATORY)
reason: "Groceries", // replace with what the payment is for
transactionReference: "TXN_" + Date.now(), // optional: echoed back for your own tracking
});
if (result.success) {
console.log("Payment successful!", result.data.transactionId);
} else if (
result.message === "Payment cancelled by user!" || // consent dialog (step 1)
(result.data && result.data.error === 201) // Alipay sheet (step 2)
) {
console.log("User cancelled the payment."); // not a hard error
} else {
console.error("Payment failed:", result.message);
}
}Quick Start — Bonga PayBill (v2)
Pay a PayBill with Bonga points via MiniAppPay.makeBongaPayment(...). Same
signature and flow as makePayment — only the underlying M-Pesa rail differs, so
paybillNo is still mandatory.
const PAYBILL_NO = "YOUR_PAYBILL_NUMBER"; // the business/paybill number that receives the money
async function handleBongaPayment() {
const result = await MiniAppPay.makeBongaPayment({
amount: 30, // replace with the actual amount (KES)
transactionReference: "TXN_" + Date.now(), // replace with your own unique reference
reason: "Electricity bill", // replace with what the payment is for
paybillNo: PAYBILL_NO, // replace with your paybill number (MANDATORY)
});
if (result.success) {
// Bonga success data has NO transactionId: { startTime, message, statusCode }.
console.log("Payment successful!", result.data.statusCode, result.data.message);
} else if (
result.message === "Payment cancelled by user!" || // consent dialog (step 1)
(result.data && result.data.errorCode === 201) // Alipay sheet (step 2, Bonga uses errorCode)
) {
console.log("User cancelled the payment."); // not a hard error
} else {
console.error("Payment failed:", result.message);
}
}What your user sees: the two-step flow
One makePayment(...) call shows two modals in sequence, and the user can
stop at either — each gives a different response:
- Consent dialog (this library's
"Confirm Payment"modal). Tapping Cancel charges nothing and returns{ success: false, message: "Payment cancelled by user!" }. - Alipay's native PayBill sheet. Tapping Confirm in step 1 hands off to
Alipay. Authorizing returns
{ success: true, message: "Payment of KES <amount> successful", data: { transactionId, startTime } }(Bonga's success data instead carries{ startTime, message, statusCode }— notransactionId). Backing out here is a different cancel — Alipay's own"Canceled by user"withdata.error === 201(Bonga usesdata.errorCode === 201).
// ✅ Success — user authorized in step 2
{
"success": true,
"message": "Payment of KES 1 successful",
"data": { "startTime": 308088201, "transactionId": "UJE3JBDHVA" },
"transactionReference": "TXN_1791962083799"
}
// 🚫 Cancelled the consent dialog (step 1)
{
"success": false,
"message": "Payment cancelled by user!",
"data": { "message": "Payment cancelled by user!" },
"transactionReference": "TXN_1791962083799"
}Always branch on
result.successfirst. Only readresult.message/result.data.errorwhen you need to tell why it failed. See makePayment-documentation.md for every response shape.
API Reference
makePayment(...) — v2
Accepts either an options object (preferred) or legacy positional
arguments with paybillNo as the mandatory 4th argument.
// Options object
MiniAppPay.makePayment({ amount, transactionReference, reason, paybillNo });
// Positional
MiniAppPay.makePayment(amount, transactionReference, reason, paybillNo);Parameters
amount(number | string): payment amount in KES.transactionReference(string): unique id; sent as the PayBillbillReference.reason(string, optional): description shown in the consent dialog.paybillNo(string, required in v2): business/paybill number to credit.
Returns: Promise<PayResponse<PayBillSuccessData>>
makeTillPayment(...) — v2
Same shape as makePayment, but targets an M-Pesa Till (Buy Goods) number.
Accepts either an options object (preferred) or positional arguments with
tillNumber as the mandatory 4th argument.
// Options object
MiniAppPay.makeTillPayment({ amount, tillNumber, reason, transactionReference });
// Positional
MiniAppPay.makeTillPayment(amount, transactionReference, reason, tillNumber);Parameters
amount(number | string): payment amount in KES.tillNumber(string, required): Till (Buy Goods) number to credit.reason(string, optional): description shown in the consent dialog.transactionReference(string, optional): your own reference, echoed back on the response. Unlike PayBill, Buy Goods has nobillReference, so this is never sent to the bridge — it is for your bookkeeping only.
Returns: Promise<PayResponse<TillSuccessData>>
makeBongaPayment(...) — v2
Pays a PayBill using Bonga points. Same signature, flow, and PayResponse
envelope as makePayment; it just routes through Alipay's
lipaNaBongaPaybill method and returns different payload contents (see the
note below). paybillNo is mandatory.
// Options object
MiniAppPay.makeBongaPayment({ amount, transactionReference, reason, paybillNo });
// Positional
MiniAppPay.makeBongaPayment(amount, transactionReference, reason, paybillNo);Parameters
amount(number | string): payment amount in KES.transactionReference(string): unique id; sent as the PayBillbillReference.reason(string, optional): description shown in the consent dialog.paybillNo(string, required): business/paybill number to credit.
Returns: Promise<PayResponse<BongaSuccessData>>
Bonga response note: the envelope is the same PayResponse, but the
contents differ from PayBill — success data has no transactionId and
failures carry errorCode (not error):
// ✅ Success
{
"success": true,
"message": "Payment of KES 30 successful",
"data": {
"startTime": 707937862,
"message": "Dear customer, your request was processed successfully",
"statusCode": "200"
},
"transactionReference": "TXN_..."
}
// 🚫 Cancel in the Alipay sheet (201)
{
"success": false,
"message": "Canceled by user",
"data": {
"errorMessage": "Canceled by user",
"errorCode": 201,
"startTime": 709531386
},
"transactionReference": "TXN_..."
}Branch on result.success; detect cancellation with
result.data.errorCode === 201.
Bridge readiness — isAvailable, onReady, whenReady
Use these to render/execute payment code only when the Alipay webview bridge is
present. They're exposed on both the MiniAppPay global and the npm exports.
// 1. Synchronous check
if (MiniAppPay.isAvailable()) {
// AlipayJSBridge is present right now
}
// 2. Run code once the bridge is ready (handles the AlipayJSBridgeReady event).
// Returns an unsubscribe function.
const off = MiniAppPay.onReady(function () {
document.querySelector(".pay-button").disabled = false;
});
// off(); // stop listening if needed
// 3. Promise-based, with an optional timeout (ms)
const ready = await MiniAppPay.whenReady(3000);
if (ready) {
// safe to call makePayment(...)
} else {
// not inside a supported Alipay webview (timed out)
}| Function | Signature | Description |
|----------|-----------|-------------|
| isAvailable() | () => boolean | True if AlipayJSBridge exists right now. |
| onReady(cb) | (cb: () => void) => () => void | Runs cb when ready (async even if already available); returns an unsubscribe function. |
| whenReady(timeoutMs?) | (timeoutMs?: number) => Promise<boolean> | Resolves true when ready, or false if timeoutMs elapses first. Waits indefinitely when no timeout is given. |
// Raw Alipay payloads
interface PayBillSuccessData { transactionId: string; startTime?: number }
interface BongaSuccessData { startTime: number; message: string; statusCode: string }
interface PayErrorData {
error?: number; // payBill/buyGoods code (201 = user cancelled)
errorCode?: number; // lipaNaBongaPaybill code (201 = user cancelled)
errorMessage?: string;
message?: string; // library-side failure (e.g. missing paybillNo)
startTime?: number;
}
type TillSuccessData = PayBillSuccessData; // buyGoods (Till) — same shape
// Discriminated union on `success` — narrow `result.data` via `result.success`
type PayResponse<TData = any> =
| { success: true; message: string; data: TData; transactionReference: string }
| { success: false; message: string; data: PayErrorData; transactionReference: string };
// makePayment / makeTillPayment → Promise<PayResponse<PayBillSuccessData>>
// makeBongaPayment → Promise<PayResponse<BongaSuccessData>>Payment Flow
PayBill and Till follow the same shape; only the mandatory identifier and the native bridge method differ.
- Validate the identifier (v2) — fail fast if missing/blank, before any
modal (
paybillNofor PayBill,tillNumberfor Till). - Check
AlipayJSBridge— fail with"Unable to make payment."if absent. - Consent dialog (step 1, user-facing) via
AlipayJSBridge.call("confirm", ...). Cancel here →"Payment cancelled by user!". - Pay (step 2, user-facing):
- PayBill →
AlipayJSBridge.call("payBill", ...)(businessID = paybillNo,billReference = transactionReference,currency = "KES"). - Till →
AlipayJSBridge.call("buyGoods", ...)(tillNumber,amount,currency = "KES",reason— nobillReference). - Bonga PayBill →
AlipayJSBridge.call("lipaNaBongaPaybill", ...)(same params as PayBill; response contents differ — see the Bonga note in the API reference). User authorizes → success; backs out → Alipay code201.
- PayBill →
- Normalize the result into
PayResponse. hideLoadinginfinally.
Error Codes / Messages
| Scenario | message | How to detect |
|----------|-----------|---------------|
| Missing/blank paybillNo (v2) | paybillNo is required to make a payment. | message text |
| Missing/blank tillNumber (v2) | tillNumber is required to make a payment. | message text |
| Not inside an Alipay webview | Unable to make payment. | message text |
| Cancel consent dialog (step 1) | Payment cancelled by user! | message text |
| Cancel Alipay sheet (step 2, PayBill/Till) | Canceled by user | data.error === 201 |
| Cancel Alipay sheet (step 2, Bonga) | Canceled by user | data.errorCode === 201 |
Configuration
- Currency: KES (Kenyan Shillings) — fixed.
- Paybill number: supplied by the caller (v2); legacy uses a built-in default.
- Till number: supplied by the caller (v2 only;
makeTillPayment).
Documentation
See makePayment-documentation.md for the full flow, signatures, and response examples.
Contributing
Building, project layout, the ES5-safe rule, and the release process live in CONTRIBUTING.md.
License
Proprietary to Cheche Digital Games.
