← All posts

WHY NJIA

Mobile money shouldn’t cost you a sprint.

M-Pesa is how East Africa pays. Yet adding it to a product still takes days to weeks of work that has nothing to do with the product. Here is where that time goes, and how Njia gives it back.

Ask a developer in Nairobi how long their M-Pesa integration took and you’ll usually get a laugh before you get a number. The first STK push comes quickly. Everything after it is what hurts: callbacks that don’t arrive, callbacks that arrive twice, a sandbox that’s down the afternoon you need it, and a customer who swears they paid when your database says they didn’t.

We call this the integration tax: work every team pays before it can take its first shilling, and keeps paying after launch. Njia exists to remove it.

Where the time actually goes

Daraja, Safaricom’s API, gives you the rails. Everything you need to run on them safely is left to you.

1. Setup before the first request

Fetch an OAuth token and refresh it before it expires. Build the STK password from your shortcode, passkey and a timestamp in exactly the right format. Get the transaction type and callback URL right. None of it is hard, but all of it is fiddly, and the first mistake costs an afternoon.

2. Callbacks you can’t verify

The payment result reaches you as a callback to a URL you control. It carries no signature, so the usual defence is an allowlist of Safaricom’s IP addresses, and you hope that list stays current. Every endpoint that marks orders as paid is an endpoint someone can try to fake.

3. Callbacks that arrive once, twice, or never

If your server is down or slow when the callback arrives, you may simply never hear about that payment, and you have to poll for its status yourself. Duplicates happen too. A handler that isn’t built for both will, sooner or later, ship an order twice or not at all.

4. No safe way to retry

Your request to start a payment times out. Did the customer get the PIN prompt? Retry and you risk charging them twice. Don’t retry and you risk losing the sale.

5. A public URL just to develop

Callbacks need a public HTTPS address, so local development means a tunnel and re-registering the URL whenever it changes.

6. A sandbox you can’t steer

You can’t ask Daraja’s sandbox to make a customer cancel, time out, or run out of funds. So the unhappy paths, the ones that decide whether your support inbox stays quiet, usually get tested by real customers.

7. Then do it all again

Airtel Money has its own API, its own authentication and its own callback format. Add a second provider and you build a second integration.

What it looks like with Njia

One API key, one call, for any supported provider:

checkout.ts

import { Njia } from "@njia/sdk";

const njia = new Njia(process.env.NJIA_API_KEY);

const payment = await njia.payments.create(
  { channel: "mpesa", amount: 150000, phone: "254712345678", reference: "ORDER-42" },
  { idempotencyKey: "order-42" } // retry freely: same key, same payment
);

Switch "mpesa" to "airtel_money" and nothing else changes. The outcome comes back as a webhook with the same shape for every provider, signed so you can prove it came from us:

webhooks.ts

import { verifyWebhookSignature } from "@njia/sdk";

const ok = verifyWebhookSignature({
  payload: rawBody,                       // the raw body, not parsed JSON
  header: req.headers["njia-signature"],  // t=…,v1=… (HMAC-SHA256)
  secret: process.env.NJIA_WEBHOOK_SECRET,
});

Here is how each item above maps to what Njia does:

The taxWith Njia
Tokens, passwords, timestampsOne secret key. We handle provider auth.
Unsigned callbacksHMAC-signed webhooks with a timestamp, so forgeries and replays are rejected.
Missed or duplicate callbacksEvery event stored, failed deliveries retried, a log of each attempt, and one-click replay.
Unsafe retriesIdempotency keys: a retried request returns the original payment.
A tunnel for local devnjia listen streams events straight to localhost.
A sandbox you can’t steerTest numbers that cancel, time out, run out of funds, or deliver twice, every time.
One integration per providerOne API and one event format for M-Pesa and Airtel Money.

Build from localhost

The Njia CLI holds a connection to our API and forwards every event to your machine as a signed webhook. No tunnel, no public URL, and your verification code runs exactly as it will in production.

$ npm install -g @njia/cli
$ njia listen --forward-to http://localhost:3000/webhooks

15:54:10  payment.succeeded  pay_p3x60w…  1500.00 KES · 254700000000
        ↳ 200 3ms  POST http://localhost:3000/webhooks

Test the unhappy paths on purpose

Our sandbox has six test numbers, each with fixed behaviour. Your error handling gets exercised before launch instead of discovered after it.

PhoneWhat happens
254700000000Succeeds after about 2 seconds
254700000001Customer cancels the PIN prompt
254700000002Times out after about 8 seconds
254700000003Insufficient funds
254700000004Succeeds slowly, after about 10 seconds
254700000005Succeeds, and the webhook is delivered twice

That last one matters most. If your handler marks an order paid twice when the same event arrives twice, you want to find out today, in test mode, not from a customer.

Your paybill. Your money.

Njia is an integration layer, not a wallet. When you go live you connect your own paybill or till, and money settles directly to your business as it does today. Your provider credentials are encrypted at rest. Pricing is a flat monthly fee plus a flat fee per payment, never a percentage of what you collect.

Who it’s for

  • Startups that want to take payments this week and spend the saved time on the product.
  • Agencies and freelancers who integrate mobile money for several clients. Separate workspaces keep each client’s keys, credentials and history apart, so you can hand a project over cleanly.
  • Teams already running Daraja who are tired of debugging callbacks and want signed webhooks, retries and replay without building them.
Njia is Swahili for “the way”. We want to clear the way between your idea and your first payment.

Try it in five minutes.

The sandbox is free, with no paybill or credit card needed.

Get your free test key