Singularity Payments LogoSingularity Payments
Getting Started

Quick Start

Get started with M-Pesa payments in your app in minutes.

Installation

Install both the Client and Server package client package

Server-Side

npm install @singularity-payments/nextjs
pnpm add @singularity-payments/nextjs
yarn add @singularity-payments/nextjs
bun add @singularity-payments/nextjs
npm install @singularity-payments/sveltekit
pnpm add @singularity-payments/sveltekit
yarn add @singularity-payments/sveltekit
bun add @singularity-payments/sveltekit
npm install @singularity-payments/nuxt
pnpm add @singularity-payments/nuxt
yarn add @singularity-payments/nuxt
bun add @singularity-payments/nuxt
npm install @singularity-payments/elysia
pnpm add @singularity-payments/elysia
yarn add @singularity-payments/elysia
bun add @singularity-payments/elysia
npm install @singularity-payments/fastify
pnpm add @singularity-payments/fastify
yarn add @singularity-payments/fastify
bun add @singularity-payments/fastify
bash npm install @singularity-payments/fastify
bash pnpm add @singularity-payments/fastify
bash yarn add @singularity-payments/fastify
bash bun add @singularity-payments/fastify
npm install @singularity-payments/hono
pnpm add @singularity-payments/hono
yarn add @singularity-payments/hono
bun add @singularity-payments/hono

Client-Side

Security Warning

Client-side packages require a backend server. Never expose M-Pesa credentials in client-side code, these packages are designed to simplify integration with the backend

npm install @singularity-payments/react
pnpm add @singularity-payments/react
yarn add @singularity-payments/react
bun add @singularity-payments/react
npm install @singularity-payments/vue
pnpm add @singularity-payments/vue
yarn add @singularity-payments/vue
bun add @singularity-payments/vue
npm install @singularity-payments/svelte
pnpm add @singularity-payments/svelte
yarn add @singularity-payments/svelte
bun add @singularity-payments/svelte

Setup Ngrok for Testing

M-Pesa requires a public callback URL to send payment notifications. For local development, use ngrok:

Install ngrok from ngrok.com or using a node package manager:

npm install -g ngrok
pnpm add -g ngrok
yarn global add ngrok
bun add -g ngrok

Start ngrok on your dev port:

ngrok http 3000

Copy the forwarding URL (e.g., https://abc123.ngrok-free.app)

Keep ngrok running while testing. M-Pesa callbacks will fail without it.

Environment Setup

Create a .env.local file in your project root:

.env.local
MPESA_CONSUMER_KEY=your_consumer_key
MPESA_CONSUMER_SECRET=your_consumer_secret
MPESA_SHORTCODE=174379 #For STK Push Testing in Sandbox(Replace with your shortcode in production)
MPESA_PASSKEY=your_passkey
MPESA_ENVIRONMENT=sandbox # or 'production'
APP_URL=https://your-app-url.com

Never commit your .env file to version control. Add it to .gitignore.

Configure M-Pesa Client

Create a server configuration file:

lib/mpesa.ts
import { createMpesa } from "@singularity-payments/nextjs";

export const mpesa = createMpesa(
  {
    consumerKey: process.env.MPESA_CONSUMER_KEY!,
    consumerSecret: process.env.MPESA_CONSUMER_SECRET!,
    passkey: process.env.MPESA_PASSKEY!,
    shortcode: process.env.MPESA_SHORTCODE!,
    environment:
      (process.env.MPESA_ENVIRONMENT as "sandbox" | "production") || "sandbox",
    callbackUrl: `${process.env.NEXT_PUBLIC_APP_URL}/api/mpesa/callback`,
  },
  {
    callbackOptions: {
      onSuccess: async (data) => {
        console.log("Payment successful:", data);
      },
      onFailure: async (data) => {
        console.log("Payment failed:", data);
      },
    },
  },
);
lib/mpesa.ts
import { createMpesa } from '@singularity-payments/sveltekit';
import {
  MPESA_CONSUMER_KEY,
  MPESA_CONSUMER_SECRET,
  MPESA_PASSKEY,
  MPESA_SHORTCODE
} from '$env/static/private';

export const mpesa = createMpesa(
  {
    consumerKey: MPESA_CONSUMER_KEY,
    consumerSecret: MPESA_CONSUMER_SECRET,
    passkey: MPESA_PASSKEY,
    shortcode: MPESA_SHORTCODE,
    environment: 'sandbox',
    callbackUrl: `https://your-domain.ngrok-free.dev/api/mpesa/callback`
  },
  {
    callbackOptions: {
      onSuccess: async (data) => {
        console.log('Payment successful:', data);
      },
      onFailure: async (data) => {
        console.log('Payment failed:', data);
      }
    }
  }
);
server/utils/mpesa.ts
import { createMpesa } from "@singularity-payments/nuxt";

const config = useRuntimeConfig();

export const mpesa = createMpesa(
  {
    consumerKey: config.mpesaConsumerKey,
    consumerSecret: config.mpesaConsumerSecret,
    passkey: config.mpesaPasskey,
    shortcode: config.mpesaShortcode,
    environment: "sandbox",
    callbackUrl: `https://your-domain.ngrok-free.dev/api/mpesa/callback`,
  },
  {
    callbackOptions: {
      onSuccess: async (data) => {
        console.log("Payment successful:", data);
      },
      onFailure: async (data) => {
        console.log("Payment failed:", data);
      },
    },
  },
);
import { Elysia } from "elysia";
import { cors } from "@elysiajs/cors";
import { createMpesa } from "@singularity-payments/elysia";

const mpesa = createMpesa({
consumerKey: process.env.MPESA_CONSUMER_KEY!,
consumerSecret: process.env.MPESA_CONSUMER_SECRET!,
passkey: process.env.MPESA_PASSKEY!,
shortcode: process.env.MPESA_SHORTCODE!,
environment: "sandbox",
callbackUrl: `https://your-domain.ngrok-free.dev/api/mpesa/callback`,
});

const app = new Elysia()
.use(
cors({
origin: "http://localhost:3000",
credentials: true,
allowedHeaders: ["Content-Type", "Authorization"],
methods: ["GET", "POST", "OPTIONS"],
}),
)
.get("/", () => "M-Pesa Payment API")
.get("/health", () => ({ status: "ok" }))
.group("/api/mpesa", (app) => app.use(mpesa.app))
.listen(3004);

console.log("Registered routes:");
app.routes.forEach((route) => {
console.log(`${route.method} ${route.path}`);
});

console.log(
`Elysia is running at ${app.server?.hostname}:${app.server?.port}`,
);
import "dotenv/config";
import express from "express";
import cors from "cors";
import { createMpesa } from "@singularity-payments/express";

const app = express();

app.use(
  cors({
    origin: ["http://localhost:3000", "http://localhost:3001"],
    credentials: true,
    methods: ["GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH"],
    allowedHeaders: ["Content-Type", "Authorization", "X-Requested-With"],
    preflightContinue: false,
    optionsSuccessStatus: 204,
  }),
);

app.use(express.json());
app.use(express.urlencoded({ extended: true }));

app.use((req, res, next) => {
  console.log(`${req.method} ${req.path}`, req.body);
  next();
});

const mpesa = createMpesa({
  consumerKey: process.env.MPESA_CONSUMER_KEY!,
  consumerSecret: process.env.MPESA_CONSUMER_SECRET!,
  passkey: process.env.MPESA_PASSKEY!,
  shortcode: process.env.MPESA_SHORTCODE!,
  environment: "sandbox",
  callbackUrl: `https://your-domain.ngrok-free.dev/api/mpesa/callback`,
});

const mpesaRouter = express.Router();
mpesa.router(mpesaRouter);
app.use("/api/mpesa", mpesaRouter);

app.use((err, req, res, next) => {
  console.error("Error:", err);
  res.status(500).json({
    success: false,
    error: err.message || "Internal server error",
  });
});

app.listen(3001, () => {
  console.log("Server running on port 3001");
  console.log("Environment variables loaded:", {
    hasConsumerKey: !!process.env.MPESA_CONSUMER_KEY,
    hasConsumerSecret: !!process.env.MPESA_CONSUMER_SECRET,
    hasPasskey: !!process.env.MPESA_PASSKEY,
    hasShortcode: !!process.env.MPESA_SHORTCODE,
  });
});
import Fastify from "fastify";
import cors from "@fastify/cors";
import { config } from "dotenv";
import { createMpesa } from "@singularity-payments/fastify";

config();

const fastify = Fastify({
logger: true,
});

fastify.register(cors, {
origin: "http://localhost:3000",
credentials: true,
});

const mpesa = createMpesa({
consumerKey: process.env.MPESA_CONSUMER_KEY!,
consumerSecret: process.env.MPESA_CONSUMER_SECRET!,
passkey: process.env.MPESA_PASSKEY!,
shortcode: process.env.MPESA_SHORTCODE!,
environment: "sandbox",
callbackUrl: `https://your-domain.ngrok-free.dev/api/mpesa/callback`,
});

fastify.register(
async (instance) => {
mpesa.router(instance);
},
{ prefix: "/api/mpesa" },
);

const start = async () => {
try {
await fastify.listen({ port: 3003, host: "0.0.0.0" });
console.log("Server running on port 3003");
} catch (err) {
fastify.log.error(err);
process.exit(1);
}
};

start();
import { Hono } from "hono";
import { serve } from "@hono/node-server";
import { config } from "dotenv";
import { createMpesa } from "@singularity-payments/hono";
import { cors } from "hono/cors";

config();

const app = new Hono();

app.use("/api/*", cors());
app.use(
  "/api/*",
  cors({
    origin: "http://localhost:3000",
    allowHeaders: ["X-Custom-Header", "Upgrade-Insecure-Requests"],
    allowMethods: ["POST", "GET", "OPTIONS"],
    exposeHeaders: ["Content-Length", "X-Kuma-Revision"],
    maxAge: 600,
    credentials: true,
  }),
);

const mpesa = createMpesa({
  consumerKey: process.env.MPESA_CONSUMER_KEY!,
  consumerSecret: process.env.MPESA_CONSUMER_SECRET!,
  passkey: process.env.MPESA_PASSKEY!,
  shortcode: process.env.MPESA_SHORTCODE!,
  environment: "sandbox",
  callbackUrl: `https://your-domain.ngrok-free.dev/api/mpesa/callback`,
});

app.route("/api/mpesa", mpesa.app);

serve({
  fetch: app.fetch,
  port: 3002,
});

console.log("Server running on port 3002");

Create Catch All Route Route ( Only for Next.js SvelteKit, Nuxt.js)

Create a catch-all API route to handle all M-Pesa operations. This only applies to:

  • SvelteKit
  • Next.js
  • Nuxt.js
app/api/mpesa/[...mpesa]/route.ts
import { mpesa } from "~/lib/mpesa";

export const { POST } = mpesa.handlers.catchAll;
src/routes/api/mpesa/[...path]/+server.ts
import { mpesa } from '$lib/mpesa';

export const POST = mpesa.handlers.catchAll.POST;
server/api/mpesa/[...path].post.ts
import { mpesa } from "../../utils/mpesa";

export default mpesa.handlers.catchAll;

This single route handles all M-Pesa operations including callbacks, payment initiation, and status queries.

Make the client hander

lib/mpesa-client.ts
import { createMpesaClient } from "@singularity-payments/react";

export const mpesaClient = createMpesaClient({
  baseUrl: "http://localhost:3000", // your backend url(Use "" if the backend and frontend are on the same port)
});
lib/mpesa-client.ts
import { createMpesaClient } from "@singularity-payments/svelte";

export const mpesaClient = createMpesaClient({
  baseUrl: "http://localhost:3000", // your backend url(Use "" if the backend and frontend are on the same port)
});
lib/mpesa-client.ts
import { createMpesaClient } from "@singularity-payments/vue";

export const mpesaClient = createMpesaClient({
  baseUrl: "http://localhost:3000", // your backend url(Use "" if the backend and frontend are on the same port)
});

Initiate an STK Push

Create a client component for handling payments:

// make sure to import mpesaClient
const { data, error } = await mpesaClient.stkPush({
     amount: 1,
     phoneNumber: phone,
     accountReference: "singularity",
     transactionDesc: "Quick Start",
   });

Example

"use client";
import { mpesaClient } from "~/lib/mpesa-client";

export default function PaymentForm() {
  const handlePay = async () => {
    const { data, error } = await mpesaClient.stkPush({
      amount: 1,
      phoneNumber: "+254740185793",
      accountReference: "singularity",
      transactionDesc: "Quick Start",
    });
  };
  return (
    <div>
      <button onClick={handlePay}>Pay</button>
    </div>
  );
}

Test Your Integration

Start your development server

npm run dev

Start ngrok in another terminal

ngrok http 3000

Update your .env.local with the ngrok URL and restart your dev server

Navigate to your page with payment integrated

Initiate the transaction and check your phone for the STK Push prompt

Enter your M-Pesa PIN to complete the payment

In sandbox mode, use the test credentials provided by Safaricom Daraja.

What's Next

Edit on GitHub

On this page