paylod
Secure integration

Guides

Secure integration

.md

Six rules that make it impossible for a payer with an intercepting proxy to undercut you, and why the amount must stay on your server.

The question everyone asks

Could a customer open my checkout in an intercepting proxy — Burp, mitmproxy — and lower the amount before it reaches M-Pesa?

Under a correct integration, no.

The API key that calls /collect authenticates you, the merchant — not the payer. Only amount and phone come from the request body; your shortcode, passkey and Daraja credentials are pulled from encrypted storage server-side and never travel over the wire. Settlement is bound to the amount your server initiated, and every webhook is HMAC-signed and reports the true settled amount.

There is exactly one way to be undercut: hand the payer control of both the key and the amount — i.e. call /collect straight from a browser or mobile app with a client-supplied amount. Then a payer can edit the request and pay less. The platform will faithfully report the true, lower amount, but it cannot rescue a merchant who trusted the client with it.

Keep the amount on your server and the attack disappears.

The short version

DoDon't
Keep mp_live_… / mp_test_… server-side onlyEmbed the API key in a browser, mobile app, or any client
Derive amount from your own order recordForward a client-supplied amount verbatim to /collect
Verify x-webhook-signature before reading the bodyTrust an unverified webhook
Confirm amount matches the order and status === "success" before fulfillingFulfil on status alone
Send one Idempotency-Key per logical orderRetry a logical order with a fresh key
Expect 429s and back offHammer on rate limits

1. Keys are server-only secrets

Never expose mp_live_… or mp_test_… in a browser, mobile app, or any client. Keys live only on your server. A client-embedded key is fully compromised — treat it as public. Anyone holding it can call your account.

2. Your server owns the amount

Your server is the sole authority on the amount. Derive it from your own order or price record; never pass a client-supplied amount straight through to /collect. If the client both holds the key and sets the amount, a payer can lower it — that is the one and only way to be undercut.

3. Verify, then check the amount, before fulfilling

On the webhook: verify the signature first, then confirm the amount and the status before fulfilling. paylod.webhookHandler() does the verification for you, so your handler only runs on an authentic event.

fulfil.ts
import { Paylod } from "@paylod/node";

const paylod = new Paylod(process.env.PAYLOD_API_KEY!);

export const POST = paylod.webhookHandler(async (event) => {
  if (event.type !== "payment.success") return;

  // Look the order up by paymentId — the id you stored when you called collect().
  const order = await db.orders.findByPaymentId(event.data.paymentId);
  if (!order) return;

  // Never fulfil on status alone. Check the TRUE settled amount.
  if (event.data.amount === order.amountKes) {
    await fulfil(order);                    // paid in full
  } else {
    await flagForReview(order, event.data); // underpaid / mismatch
  }
});

The webhook always carries the true settled amount, so any partial or underpayment is visible the moment you check it.

Store ack.paymentId against your order when you call collect(), and key the lookup on it. The event carries paymentId, accountRef and the M-Pesa fields. It does not echo back the metadata you sent to collect(), so do not build your fulfilment around that.

Make the handler idempotent. Deliveries are retried until you return 2xx, and a repeat of an event you have already handled is normal rather than an error.

4. Treat callback and mint tokens as secrets

The callback URL and the mint token are secrets. Don't log them, don't expose them client-side — they belong on your server, alongside the API key.

5. One idempotency key per order

Send an Idempotency-Key per logical order so retries never double-charge. Reusing a key with a different body is rejected with 409 — a guard against accidental key reuse across orders.

6. Expect rate limits

/collect is rate-limited per API key and per phone number. Expect 429 Too Many Requests under bursts and back off — retry with the same Idempotency-Key after a short delay.


Follow these six rules and a payer wielding an intercepting proxy cannot undercut you, over-charge you, redirect funds, or trick you into fulfilling an order that was not paid in full. The platform is safe by design — it just needs the amount to stay on your server.