cal-me-j / jvoex labs

Stop rebuilding M-Pesa integration from scratch, every project.

A TypeScript engine for Paystack, Flutterwave, and M-Pesa STK Push — signed webhooks, phone-number normalization, and background reconciliation for the transactions that silently get stuck in PENDING.

STK Push prompt sent, callback never arrives
Phone number arrives as 07..., 254..., or +254...
Webhook body trusted without verifying it came from the provider

Three failure points, solved once

Pulled directly from integrating these providers in production — the parts that cost real debugging time, not the parts covered by the providers' own docs.

Dropped webhooks A user enters their PIN late, the network stalls, and the callback that was supposed to confirm payment never reaches your server. The transaction sits in PENDING forever unless something actively checks on it.
// background reconciliation, runs on a schedule
async function reconcilePendingTransactions() {
  const pending = await db.transaction.findMany({ where: { status: 'PENDING' } });
  for (const tx of pending) {
    const result = await queryMpesaExpressStatus(tx.checkoutRequestId);
    if (result.ResultCode === '0') {
      await yourFulfilmentHook(tx); // grant the purchase first
      await db.transaction.update({ where: { id: tx.id }, data: { status: 'SUCCESS' } }); // only then mark settled
    }
  }
}
Inconsistent phone formats M-Pesa and regional gateways require strict E.164 (+254XXXXXXXXX). Users type 0712..., 254712..., or the full international format interchangeably — and a bad regex fails silently mid-request.
export function normalizeKenyanPhone(phone: string): string {
  let cleaned = phone.replace(/[^\d+]/g, '');
  if (cleaned.startsWith('0')) cleaned = `+254${cleaned.slice(1)}`;
  else if (cleaned.startsWith('254')) cleaned = `+${cleaned}`;
  else if (!cleaned.startsWith('+254')) throw new Error(`Invalid phone: ${phone}`);
  return cleaned;
}
Unverified webhook payloads Anyone who finds your webhook URL can POST a fake "payment succeeded" body unless you cryptographically verify it came from the provider — before it ever reaches your database.
export const PaystackWebhookSchema = z.object({
  event: z.string(),
  data: z.object({
    status: z.enum(['success', 'failed', 'abandoned']),
    reference: z.string(),
    amount: z.number(),
    metadata: z.object({ tenantId: z.string() }).passthrough(),
  }),
});
// + raw-buffer HMAC signature check before this schema ever runs
PENDING → checked on schedule SUCCESS → confirmed by provider, not assumed

Pick what you need

Each tier includes everything below it. Single-developer commercial license; resale or public re-hosting of the source isn't permitted.

Tier 1 — Data Contracts

Validation schemas & types

  • Zod schemas for Paystack / Flutterwave / M-Pesa payloads
  • Full TypeScript types for transaction and webhook shapes
$19
Tier 2 — Core Engine

Everything to integrate, standalone

  • Everything in Tier 1
  • Phone normalizer, signed-webhook verification, fetch-based gateway client
  • Working Express sandbox route to test against immediately
$49
Tier 3 — Multi-Tenant Bundle

For SaaS platforms & ERP modules

  • Everything in Tiers 1 & 2
  • Multi-tenant Prisma schema, reconciliation script with a fulfilment hook you control
  • Production Dockerfile + PWA checkout frontend
$99