TypeScript library for the M-Pesa STK Push lifecycle with built-in webhook relay server.
# mpesa-stk
M-PESA for TypeScript, in two packages you can use separately or together.
| Package | Covers | Use it when |
|---|---|---|
| **`mpesa-stk`** (this one) | STK Push, end to end | You need the STK lifecycle done properly — idempotency, dedup, a poll fallback, reconciliation, a signing relay |
| **`mpesa-billing`** | STK Push, C2B, B2C, B2B, Stripe Checkout | You need more than one rail, settling through one exactly-once contract |
Node 18+ · Postgres · zero-config TypeScript types. Start with `mpesa-billing` if you
want the other rails; everything below is `mpesa-stk` itself.
---
The M-Pesa STK Push lifecycle, done properly. Daraja gives you "send a push" and "receive one callback" — this library is everything in between: idempotent initiation, atomic callback dedup, a polling fallback, and reconciliation.
TypeScript · Node 18+ · Postgres. STK Push only — for B2C, C2B, B2B, or cards, use `mpesa-billing`.
## Why
Daraja's docs are a request/response spec. They stop where production starts. Pointed at the live sandbox, the API actually:
- re-fires the same callback multiple times under load — dedupe or you double-credit
- drops the callback entirely if your server blinks — no retry, no dead-letter
- rate-limits the STK Query (5 req/min in sandbox) and returns undocumented transient codes like `4999` mid-flight — a naive poller marks a still-pending payment FAILED
- masks the customer's phone number in 2026+ callbacks
- has no idempotency key — a double-tapped "Pay" sends two pushes and charges twice
Each of those is a wrong refund or a missed order waiting to happen. This library is that missing layer.
## Install
```bash
npm install mpesa-stk pg
```
## Usage
```typescript
import { MpesaStk, PostgresAdapter } from 'mpesa-stk'
import { Pool } from 'pg'
const adapter = new PostgresAdapter(new Pool({ connectionString: process.env.DATABASE_URL }))
await adapter.migrate() // creates mpesa_payments; safe on every startup (IF NOT EXISTS)
const mpesa = …