Appearance
Kahawa
Kahawa is a coffee-shop menu that takes MOOKHPay. The shop is kahawa-demo, next to this repo. The server functions are the whole integration.
A customer opens the menu, posts quantities, and lands on a hosted checkout. MOOKHPay sends them back to /paid. The shop reads the checkout with its secret key and then shows a receipt.
kahawa-demo/
├── functions/
│ ├── index.ts GET / menu and basket
│ ├── order.ts POST /order create the checkout, redirect
│ └── paid.ts GET /paid confirm, then show the receipt
├── lib/menu.ts menu, prices, helpers
├── public/ styles and the MOOKHPay mark
└── wrangler.toml MOOKHPAY_APIPrices stay on the server
The browser posts quantities. lib/menu.ts is the only place a price lives.
ts
export const MENU: Item[] = [
{ id: "espresso", name: "Espresso", note: "Double shot, Kiambu single origin", amount: 100 },
{ id: "latte", name: "Latte", note: "Steamed milk, a little foam", amount: 200 },
{ id: "mandazi", name: "Mandazi", note: "Two, warm, cardamom sugar", amount: 300 },
];
export const MAX_EACH = 5;amount is minor units. These prices are KES 1, KES 2, and KES 3 because staging charges real money.
linesFrom keeps known items with a quantity from 1 to 5. totalOf sums amount * qty. describe builds the payer text, such as 2× Espresso, 1× Mandazi.
Create and redirect
functions/order.ts handles POST /order.
ts
const order = `order-${crypto.randomUUID().slice(0, 8)}`;
const shop = new URL(request.url).origin;
const res = await fetch(`${env.MOOKHPAY_API}/checkouts`, {
method: "POST",
headers: {
authorization: `Bearer ${env.MOOKHPAY_SECRET_KEY}`,
"idempotency-key": order,
"content-type": "application/json",
},
body: JSON.stringify({
amount: totalOf(lines),
reference: order,
description: describe(lines),
return_url: `${shop}/paid`,
}),
});
const body = (await res.json().catch(() => null)) as
| { ok?: boolean; description?: string; data?: { url?: string } }
| null;
if (!res.ok || !body?.data?.url) {
// show body.description, or `HTTP ${res.status}`
}
let url = body.data.url;
if (env.CHECKOUT_ORIGIN) {
const u = new URL(url);
url = env.CHECKOUT_ORIGIN.replace(/\/$/, "") + u.pathname;
}
return Response.redirect(url, 303);| Choice | Why |
|---|---|
One value for idempotency-key and reference | One id per order. A retry returns the same checkout. reference comes back on the return URL |
amount: totalOf(lines) | Price from lib/menu.ts, in minor units |
description: describe(lines) | Text on the payer page |
return_url: ${shop}/paid | Uses this request's origin, so a preview URL and the live URL both work |
res.json().catch(() => null) | A non-JSON error page does not crash the function |
303 | The next request is a GET. Back does not post the order again |
CHECKOUT_ORIGIN | Optional. Replaces the origin of data.url so a demo can use a hub preview. Omit it and redirect to data.url |
MOOKHPAY_SECRET_KEY exists only in the Pages Function. The menu HTML has no key and no price.
An empty basket does not call the API. The function shows "Your order is empty."
The payer pays
The shop writes no payment UI. The customer sees the amount and the description on the hosted page. They cannot change the amount.
Confirm
MOOKHPay can send the customer to /paid?checkout=cs_ke_…&reference=order-…&status=completed. functions/paid.ts reads only checkout.
ts
const id = new URL(request.url).searchParams.get("checkout") ?? "";
if (!/^cs_[a-z]{2}_[0-9a-f]{24}$/.test(id)) return notPaid();
const res = await fetch(`${env.MOOKHPAY_API}/checkouts/${id}`, {
headers: { authorization: `Bearer ${env.MOOKHPAY_SECRET_KEY}` },
});
const body = await res.json().catch(() => null);
const c = body?.data;
if (!res.ok || c?.status !== "completed") return notPaid();The receipt shows description, amount, reference, and payment.provider_receipt. Every other result is the same "Not paid" page. That includes open, expired, 404, and a failed fetch.
Kahawa does not store orders
It has no database. It does not poll a payer who paid and closed the tab. A reload of /paid can show the receipt again. A shop that ships goods should do both. See Confirming a payment.
Run it
You need Node and pnpm. From kahawa-demo:
sh
pnpm installCreate .dev.vars in that directory. Wrangler loads it for local dev. Git ignores it. There is no example file in the repo.
MOOKHPAY_SECRET_KEY=mkp_test_…sh
pnpm devMOOKHPAY_API is a normal variable in wrangler.toml:
toml
[vars]
MOOKHPAY_API = "https://payments-ke-staging.salimia.me/v1/payments/merchant"Deploy
Put the secret in Pages. Do not put it in wrangler.toml or in git.
sh
pnpm secret MOOKHPAY_SECRET_KEY
pnpm deploypackage.json sets CLOUDFLARE_ACCOUNT_ID on those scripts. Pages config does not take account_id. Use the same pattern when your Wrangler login can see more than one account.
Copy the pattern
- Keep prices on the server. Replace
lib/menu.tswith what you sell. - Keep the create, the idempotency key, and the redirect in
order.ts. Changedescriptionto the text the payer should see. - In
paid.ts, fulfill the order whenstatusiscompleted. Then show your own receipt.