• Сообщество
  • Блог
Документация
Плагины и интеграцииВсе расширения для Medusa от сообществаЭкспертыПодберите специалиста для разработки и развития вашего проекта на Medusa
КейсыПосмотрите примеры Medusa в продакшене и успешные внедрения
Меч Moscow
Комплексная e-commerce платформа на Medusa для московского fashion-бренда

Меч Moscow · Fashion

Нужна помощь в разработке плагина интеграции для Medusa?

Написать намНайти плагины

Gorgo снижает затраты на адаптацию Medusa к локальным рынкам.

Мы разрабатываем плагины интеграции, осуществляем поддержку и развиваем сообщество разработчиков на Medusa в Telegram.

  • Ресурсы Medusa
  • Плагины и интеграции
  • Эксперты
  • Кейсы
  • Medusa Чат в Telegram
  • Medusa Новости в Telegram
  • Документация Gorgo
  • Связаться с нами
  • TelegramGitHub
Плагины
M

Mpesa

Medusa v2 M-Pesa (Safaricom Daraja) payment provider — STK Push, polling and refunds

Нужна доработка этого плагина?

Связаться с нами
npm install medusa-payment-mpesa
Категория
Платежи
Создано
Gisioraelvis
Версия
1.0.1
Последнее обновление
8 часов назад
Ежемесячные загрузки
Загрузка данных
Звезды на Github
1
npmNPMGitHubGithub

Medusa M-Pesa Payment Provider

A Medusa v2 Payment Provider plugin for M-Pesa Daraja API (Safaricom Kenya).

Features:

  • STK Push (Lipa Na M-Pesa Online) — customer receives a payment prompt on their phone
  • Asynchronous confirmation via Daraja callback webhook + storefront status polling
  • Refunds via M-Pesa reversal (requires initiator credentials)
  • Sandbox and production environments
  • Phone number normalization — accepts , , , and bare formats
  • RSA PKCS#1 v1.5 credential encryption for production reversals (required by Safaricom Daraja API)
  • TypeScript-first with exported option types

Table of Contents

  • Requirements
  • Installation
  • Configuration
    • 1. Register the provider
    • 2. Environment variables
    • 3. Add provider to a Region
  • Payment Flow
  • Supported Phone Number Formats
  • API Routes
  • Storefront Integration
  • Refund Flow
  • Result Code Reference
  • Production Setup
  • Sandbox Testing

Requirements

  • Medusa v2 ()
  • Node.js >= 20
  • A Safaricom Daraja app with Lipa Na M-Pesa Online enabled

Installation

1npm install medusa-payment-mpesa
2# or
3pnpm add medusa-payment-mpesa
4# or
5yarn add medusa-payment-mpesa

Configuration

1. Register the provider in

1import { loadEnv, defineConfig } from "@medusajs/framework/utils";
2import type { MpesaOptions } from "medusa-payment-mpesa";
3
4loadEnv(process.env.NODE_ENV || "development", process.cwd());
5
6module.exports = defineConfig({
7 // ...
8 modules: [
9 {
10 resolve: "@medusajs/medusa/payment",
11 options: {
12 providers: [
13 {
14 resolve: "medusa-payment-mpesa/providers/mpesa",
15 id: "mpesa",
16 options: {
17 consumer_key: process.env.MPESA_CONSUMER_KEY,
18 consumer_secret: process.env.MPESA_CONSUMER_SECRET,
19 business_short_code: process.env.MPESA_BUSINESS_SHORT_CODE,
20 pass_key: process.env.MPESA_PASS_KEY,
21 // Defaults to "sandbox" if not set
22 environment: process.env.MPESA_ENVIRONMENT || "sandbox",
23 // Must be publicly reachable — Daraja POSTs callbacks here
24 callback_base_url:
25 process.env.MPESA_CALLBACK_BASE_URL || process.env.BACKEND_URL,
26 // Required only for refunds (M-Pesa reversals):
27 initiator_name: process.env.MPESA_INITIATOR_NAME,
28 initiator_password: process.env.MPESA_INITIATOR_PASSWORD,
29 } satisfies MpesaOptions,
30 },
31 ],
32 },
33 },
34 ],
35});

2. Set environment variables

1# Required
2MPESA_CONSUMER_KEY=your_consumer_key
3MPESA_CONSUMER_SECRET=your_consumer_secret
4MPESA_BUSINESS_SHORT_CODE=174379
5MPESA_PASS_KEY=your_pass_key
6MPESA_CALLBACK_BASE_URL=https://your-backend.example.com
7
8# Optional
9MPESA_ENVIRONMENT=sandbox # Default: "sandbox" | Options: "sandbox" or "production"
10MPESA_INITIATOR_NAME=testapi # Required only for refunds
11MPESA_INITIATOR_PASSWORD=Safaricom123 # Required only for refunds
12MPESA_WEBHOOK_SECRET=supersecret # Optional secret for webhook verification

Create app at Daraja Dashboard and copy the credentials.

3. Set up Kenya Region

  1. Go to Medusa Admin → Settings → Regions
  2. Click Create
  3. Configure the region:
    • Name: Kenya
    • Currency: KES (Kenyan Shilling)
    • Countries: Add Kenya under the Countries section
  4. Configure tax and payment provider settings as needed
  5. Click Save

4. Add M-Pesa Payment Provider to Kenya Region

  1. Go to Medusa Admin → Settings → Regions → Kenya → Edit
  2. Navigate to Payment Providers
  3. Add M-Pesa as a payment provider
  4. The M-Pesa provider will appear at checkout only for carts in the Kenya region

5. Configure Tax Region for Kenya

  1. Go to Medusa Admin → Settings → Tax Regions
  2. Click Create to add a new tax region
  3. Configure for Kenya with the appropriate tax rates

Payment Flow

1sequenceDiagram
2 participant Customer
3 participant Storefront
4 participant Medusa
5 participant Daraja
6
7 Storefront->>Medusa: initiatePaymentSession({ phone_number })
8 Medusa->>Daraja: POST /mpesa/stkpush/v1/processrequest
9 Daraja-->>Medusa: { CheckoutRequestID, MerchantRequestID }
10 Medusa-->>Storefront: payment session created (id = CheckoutRequestID)
11 Daraja-->>Customer: STK Push prompt on phone
12
13 Note over Customer,Daraja: Customer has ~60s to accept STK Push
14
15 Storefront->>Medusa: GET /store/mpesa/status/:checkoutRequestId
16 Medusa->>Daraja: POST /mpesa/stkpushquery/v1/query
17 Daraja-->>Medusa: { ResultCode }
18 Medusa-->>Storefront: { status: "pending" | "paid" | "cancelled" | "error" }
19
20 alt Customer paid (status: paid)
21 Daraja->>Medusa: POST /store/mpesa/callback (async, may arrive later)
22 Storefront->>Medusa: placeOrder
23 Medusa-->>Medusa: authorizePayment → status: authorized
24 else status: cancelled or error
25 Storefront-->>Customer: Show error / retry
26 else status: pending
27 Storefront->>Medusa: placeOrder (authorizePayment re-queries Daraja)
28 end

Supported Phone Number Formats

The provider normalizes all inputs to the (12-digit) format required by Daraja. Any non-digit characters (spaces, dashes, ) are stripped first.

Input FormatExampleNormalized
Local with leading 0
International
International
9-digit without prefix
New prefix

Numbers that don't match any of these patterns are rejected with a error.

The phone number can be passed in two ways — the first available is used:

  1. Explicitly via in the payment session (recommended)
  2. From the authenticated customer's saved field
1await sdk.store.payment.initiatePaymentSession(cart, {
2 provider_id: "pp_mpesa_mpesa",
3 data: {
4 phone_number: "0712345678", // any supported format
5 },
6});

API Routes (added automatically by the plugin)

All four public routes are registered automatically by the plugin without any manual configuration.

MethodPathAuthDescription
PublicDaraja STK Push result callback
PublicStorefront payment status polling
PublicDaraja reversal result callback
PublicDaraja reversal timeout callback

Configure these URLs in your Daraja app settings:

Daraja SettingValue
Callback URL
Result URL (reversals)
Queue Timeout URL

Note: The callback URLs must be publicly accessible HTTPS endpoints. For local development, use a tunneling service like ngrok.


Storefront Integration

1. Display M-Pesa in the payment method list

Add the provider to your payment info map (e.g. ):

1import { Phone } from "@medusajs/icons"
2
3export const paymentInfoMap: Record<string, { title: string; icon: JSX.Element }> = {
4 // ... other providers
5 pp_mpesa_mpesa: { title: "M-Pesa", icon: <Phone /> },
6}

2. Collect the phone number at checkout

When M-Pesa is selected, show a phone number input before the customer can submit:

1const isMpesa = (providerId?: string) => providerId === "pp_mpesa_mpesa";
2
3const [mpesaPhone, setMpesaPhone] = useState("");
4// Validates: 07XXXXXXXXX, +254XXXXXXXXX, or 254XXXXXXXXX
5const phoneValid = /^(254[0-9]{9}|0[0-9]{9}|\+254[0-9]{9})$/.test(mpesaPhone);
6
7// On checkout submit — pass phone_number in the session data
8await sdk.store.payment.initiatePaymentSession(cart, {
9 provider_id: "pp_mpesa_mpesa",
10 data: { phone_number: mpesaPhone },
11});

3. Check status before placing the order

After the payment step is submitted, the customer has ~60 seconds to accept the M-Pesa STK Push prompt on their phone. Before calling , do a single status check and surface terminal failures immediately:

1const BACKEND_URL = process.env.NEXT_PUBLIC_MEDUSA_BACKEND_URL;
2
3type MpesaStatusResponse = {
4 status: "paid" | "pending" | "cancelled" | "error";
5 result_code: string | null;
6 result_desc: string | null;
7};
8
9async function checkMpesaStatus(
10 checkoutRequestId: string,
11): Promise<MpesaStatusResponse> {
12 const res = await fetch(
13 `${BACKEND_URL}/store/mpesa/status/${encodeURIComponent(checkoutRequestId)}`,
14 );
15 return res.json();
16}
17
18// In your payment button handler:
19const { status, result_desc } = await checkMpesaStatus(checkoutRequestId);
20if (status === "cancelled" || status === "error") {
21 // Show result_desc to the customer and let them retry
22 throw new Error(result_desc ?? "Payment was not completed.");
23}
24// status "paid" or "pending" → proceed; authorizePayment re-queries Daraja on placeOrder
25await placeOrder();

Tip: The status endpoint can also be called in a polling loop (e.g. every 3 s for up to 90 s) if you want to give the customer real-time feedback while they interact with the STK Push prompt, before they click "Place order" e.g. show a message like that updates every poll. Break early if status becomes , or abort if it becomes /. If it reaches 90 s with no success, let the customer click "Place order" anyway — the final server-side will do one last status check to confirm before placing the order. See in storefront for reference implementation.


Refund Flow

Refunds are processed as M-Pesa reversals and are asynchronous — Daraja sends the result to after processing. The (from the original payment callback) is required.

1sequenceDiagram
2 participant Admin
3 participant Medusa
4 participant Daraja
5
6 Admin->>Medusa: Create refund (order management)
7 Medusa->>Medusa: refundPayment — reads mpesa_receipt_number
8 Medusa->>Daraja: POST /mpesa/reversal/v1/request
9 Note over Medusa,Daraja: SecurityCredential = RSA PKCS#1 v1.5<br/>initiator_password (production)<br/>base64 (sandbox)
10 Daraja-->>Medusa: { ConversationID, ResponseCode: "0" }
11 Medusa-->>Admin: Refund initiated (ConversationID stored)
12
13 alt Reversal succeeds
14 Daraja->>Medusa: POST /store/mpesa/reversal-result (ResultCode 0)
15 Medusa-->>Medusa: Log success
16 else Reversal times out
17 Daraja->>Medusa: POST /store/mpesa/reversal-timeout
18 Medusa-->>Medusa: Log warning
19 end

Note: A refund will fail if is not present in the payment session data. This value is populated by the STK Push callback when the customer pays. If the callback was not received, manual reconciliation with Safaricom is required.


Result Code Reference

These are the M-Pesa STK Push result codes returned by Daraja and how this plugin maps them:

Result CodeMeaning response status
Success
Request cancelled by user
Timeout — user did not respond (terminal)
Wrong PIN entered (terminal)
Transaction expired (terminal)
Internal switch error (terminal)
otherTransaction not yet settled

Terminal codes (, , , , ) will never succeed on retry and are mapped to immediately rather than being polled again.


Production Setup

1. RSA certificate for reversals

Production M-Pesa reversals require the initiator password to be encrypted with Safaricom's public certificate using RSA PKCS#1 v1.5 (). This is Safaricom's mandated scheme for the field — it is not configurable. Download from the Safaricom Developer Portal and place it in the root of your Medusa backend (same directory as , where resolves).

The plugin handles encryption automatically — sandbox uses base64, production uses RSA PKCS#1 v1.5. You do not need to encrypt it yourself.

2. Make the callback URL publicly accessible

must be an HTTPS URL reachable from Safaricom's servers. For local development, use a tunneling service:

1ngrok http 9000
2# Then set: MPESA_CALLBACK_BASE_URL=https://xxxx.ngrok.io

3. STK Push limit

The field in STK Push requests is capped at 12 characters by Daraja. If you pass in the payment session data, it will be truncated automatically. This is purely a Daraja label shown on the customer's M-Pesa statement.

1await sdk.store.payment.initiatePaymentSession(cart, {
2 provider_id: "pp_mpesa_mpesa",
3 data: {
4 phone_number: "0712345678",
5 order_id: cart.id, // truncated to 12 chars — used as AccountReference
6 },
7});

Sandbox Testing

  1. Log in to the Daraja Developer Portal and create a sandbox app
  2. Use the sandbox credentials:
    • Short code:
    • Test phone:
    • Passkey: available on the Daraja portal under your app
  3. Use the portal's Simulate tab to trigger an STK Push callback without a real phone
  4. Confirm your callback URL is reachable (use ngrok locally) before testing

License

MIT © Elvis Gisiora


Built with Medusa v2 and the Safaricom Daraja API.

Еще в этой категории

Посмотреть все
Платежи
Braintree logo

Braintree

От Lambda Curry

Поддержка платежей и 3D Secure через Braintree

Загрузка данных
GitHubnpm
Платежи
Pay. logo

Pay.

От Webbers

Принимайте кредитные карты, цифровые платежи и купи сейчас — плати потом

Загрузка данных
GitHubnpm
Платежи
Mollie logo

Mollie

От Variable Vic

Легко принимайте мультивалютные платежи через Mollie

Загрузка данных
GitHubnpm