View as Markdown llms.txt

Going live#

Production means real money and irreversible mistakes. Read this before switching DARAJA_MODE.

Before you switch#

Get production credentials. In the Daraja portal, take your app through Go Live. You get a different consumer key and secret, and your own shortcode. None of your sandbox values carry over.

Ratiba needs a contract. M-Pesa Ratiba is a commercial API. You email apisupport@safaricom.co.ke, Safaricom's commercial team discusses terms, you sign, and only then is it enabled on your shortcode. Pricing at time of writing is 5% of transaction value capped at KES 5 per standing order executed, exclusive of VAT, on top of the normal C2B tariff.

Generate your own security credential. The portal gives you a pre-encrypted initiator password. It works, and it silently stops working when Safaricom rotates the certificate. Generating it locally makes rotation a config change:

ts
import { encryptSecurityCredential } from 'daraja-mcp';
import { readFileSync } from 'node:fs';

const cert = readFileSync('./ProductionCertificate.cer', 'utf8');
console.log(encryptSecurityCredential('your-initiator-password', cert));

The certificate is on the portal under the API docs. Note that Safaricom's cert uses PKCS#1 v1.5 padding, not OAEP.

Callback security#

This is the part that actually matters.

Daraja callbacks are unsigned HTTP POSTs that change payment state. There is no HMAC, no shared secret in the body, nothing to verify. If your callback URL is reachable and unprotected, anyone who guesses it can tell your system a payment succeeded when no money moved.

Three controls, all on by default in production:

Source verification. Only Safaricom's published egress ranges are accepted. The server will not start in production with DARAJA_CALLBACK_ALLOW_ANY_IP set. If Safaricom adds ranges, override with DARAJA_CALLBACK_CIDRS.

Path secret. Set DARAJA_CALLBACK_PATH_SECRET to a long random string. Callback URLs become /cb/<secret>/stk, so the endpoint is unguessable even if someone learns your hostname. Compared in constant time.

bash
export DARAJA_CALLBACK_PATH_SECRET=$(openssl rand -hex 32)

Proxy trust. The server reads X-Forwarded-For only when it is behind a proxy you have told it to trust. If you terminate TLS yourself with nothing in front, that header is attacker-controlled and must not be trusted.

Reconciliation#

Callbacks get lost. The tunnel drops, your process restarts mid-delivery, Safaricom's retry budget runs out. Do not treat a missing callback as a failed payment.

Callbacks are stored on disk. Append-only JSON Lines at DARAJA_CALLBACK_STORE_DIR, reloaded on restart, so a crash does not lose the record of a settled payment. Back this up alongside your database.

Query before retrying. If a payment's outcome is unknown, call transaction_status rather than resending. Resending an STK push to a customer who already paid is how you double-charge someone.

Backfill with pull transactions. After an outage, pull_transactions fetches C2B transactions for a window so you can reconcile what you missed.

Watch for the two code spaces. A synchronous ResponseCode of 0 means "accepted for processing", not "paid". The callback's ResultCode of 0 means paid. Conflating them means marking orders paid that never were. Ratiba makes this worse by using 200 for sync success and 0 for callback success.

Operational checklist#

What this server does not do#

It is a Daraja client, not a ledger. It does not decide whether a payment should happen, hold balances, or reconcile against your books. Those belong in your system, where the money lives.