# mpesa-snap

A lightweight Node.js library for integrating Safaricom M-Pesa APIs into your application.

Supports **STK Push**, **B2C payments**, **C2B URL registration**, **standing orders (Ratiba)**, and **OAuth authentication** — for both sandbox and production environments.

---

## Table of Contents

- [Installation](#installation)
- [Getting Your Credentials](#getting-your-credentials)
- [Quick Start](#quick-start)
- [Importing](#importing)
- [API Reference](#api-reference)
  - [Mpesa class](#mpesa-class-recommended)
  - [stkPush](#stkpush)
  - [b2c](#b2c)
  - [registerC2B](#registerc2b)
  - [ratiba](#ratiba)
  - [auth](#auth)
- [Handling Callbacks](#handling-callbacks)
- [Error Handling](#error-handling)
- [Going to Production](#going-to-production)

---

## Installation

```bash
npm install mpesa-snap
```

Requires Node.js 16 or later.

---

## Getting Your Credentials

Before you write any code, you need credentials from Safaricom.

1. Go to [developer.safaricom.co.ke](https://developer.safaricom.co.ke) and create an account.
2. Create a new app and note your **Consumer Key** and **Consumer Secret**.
3. For STK Push (Lipa Na M-Pesa), go to the **Lipa Na M-Pesa** section and copy your **Passkey**.
4. Use the **sandbox** environment while testing — no real money moves.

| Credential | Where to find it |
|---|---|
| `consumer_key` | App details page on the developer portal |
| `consumer_secret` | App details page on the developer portal |
| `shortCode` | Sandbox: use `174379`. Production: your Paybill/Till number |
| `password` (passkey) | Sandbox: from the Lipa Na M-Pesa sandbox section. Production: from your M-Pesa portal |

---

## Quick Start

The fastest way to accept a payment is with the `Mpesa` class and `stkPush`:

```ts
import { Mpesa } from 'mpesa-snap';

const mpesa = new Mpesa(
  {
    consumer_key: process.env.MPESA_CONSUMER_KEY!,
    consumer_secret: process.env.MPESA_CONSUMER_SECRET!,
    shortCode: '174379',
    password: process.env.MPESA_PASSKEY!,
  },
  'sandbox'
);

// Send a payment prompt to the customer's phone
const response = await mpesa.stkPush({
  amount: 1,
  phoneNumber: '254712345678',
  callBackUrl: 'https://yourapp.com/mpesa/callback',
  accountRef: 'Order-001',
  description: 'Payment for order',
});

console.log(response.data);
// { MerchantRequestID: '...', CheckoutRequestID: '...', ResponseCode: '0', ... }
```

The customer receives a PIN prompt on their phone. Once they enter their PIN, M-Pesa sends the result to your `callBackUrl`.

> **Tip:** Store your credentials in environment variables, never hardcode them.

---

## Importing

**CommonJS**

```js
const { Mpesa, stkPush, b2c, auth, ratiba, registerC2B } = require('mpesa-snap');
```

**ES Modules / TypeScript**

```ts
import { Mpesa, stkPush, b2c, auth, ratiba, registerC2B } from 'mpesa-snap';
```

---

## API Reference

### `Mpesa` class (recommended)

Bundles your credentials so you don't repeat them on every call. Best for applications that use a single business account.

```ts
const mpesa = new Mpesa(credentials, environment);
```

**Constructor parameters**

| Parameter | Type | Description |
|---|---|---|
| `consumer_key` | `string` | App consumer key |
| `consumer_secret` | `string` | App consumer secret |
| `shortCode` | `string \| number` | Business short code |
| `password` | `string` | Lipa Na M-Pesa passkey |
| `environment` | `"sandbox" \| "production"` | Second argument to the constructor |

#### `mpesa.stkPush(data)`

Triggers a payment prompt (Lipa Na M-Pesa Online / STK Push) on the customer's phone.

```ts
const response = await mpesa.stkPush({
  amount: 500,
  phoneNumber: '254712345678',   // must start with 254
  callBackUrl: 'https://yourapp.com/mpesa/callback',
  accountRef: 'Order-001',       // shows on customer receipt
  description: 'Payment',        // optional
});
```

---

### `stkPush`

Standalone function — useful when you manage credentials per-request.

```ts
import { stkPush } from 'mpesa-snap';

const response = await stkPush({
  consumer_key: 'YOUR_CONSUMER_KEY',
  consumer_secret: 'YOUR_CONSUMER_SECRET',
  environment: 'sandbox',
  shortCode: '174379',
  password: 'YOUR_PASSKEY',
  amount: 100,
  phoneNumber: '254712345678',
  callBackUrl: 'https://yourapp.com/mpesa/callback',
  accountReference: 'INV-001',
  transactionType: 'CustomerPayBillOnline', // or 'CustomerBuyGoodsOnline'
});
```

| Field | Type | Required | Description |
|---|---|---|---|
| `consumer_key` | `string` | Yes | App consumer key |
| `consumer_secret` | `string` | Yes | App consumer secret |
| `environment` | `"sandbox" \| "production"` | Yes | Target environment |
| `shortCode` | `string \| number` | Yes | Business short code |
| `password` | `string` | Yes | Lipa Na M-Pesa passkey |
| `amount` | `number` | Yes | Amount to charge |
| `phoneNumber` | `string` | Yes | Customer phone in `254...` format |
| `callBackUrl` | `string` | Yes | Webhook URL that receives the payment result |
| `accountReference` | `string` | No | Reference shown on customer receipt |
| `description` | `string` | No | Transaction description (default: `"Bill payment"`) |
| `transactionType` | `string` | No | `CustomerPayBillOnline` or `CustomerBuyGoodsOnline` |

---

### `b2c`

Sends money from your business to a customer's M-Pesa wallet (disbursements, salaries, refunds).

```ts
import { b2c } from 'mpesa-snap';

const response = await b2c({
  consumer_key: 'YOUR_CONSUMER_KEY',
  consumer_secret: 'YOUR_CONSUMER_SECRET',
  environment: 'sandbox',
  shortCode: '600000',
  security_credentials: 'ENCRYPTED_CREDENTIAL', // encrypt initiator password with M-Pesa public key
  amount: 500,
  phoneNumber: '254712345678',
  commandID: 'BusinessPayment',
  resultCallbackUrl: 'https://yourapp.com/mpesa/b2c/result',
  timeoutCallbackUrl: 'https://yourapp.com/mpesa/b2c/timeout',
  remarks: 'Refund',
});
```

| Field | Type | Required | Description |
|---|---|---|---|
| `consumer_key` | `string` | Yes | App consumer key |
| `consumer_secret` | `string` | Yes | App consumer secret |
| `environment` | `"sandbox" \| "production"` | Yes | Target environment |
| `shortCode` | `string \| number` | Yes | Business short code |
| `security_credentials` | `string` | Yes | Initiator password encrypted with M-Pesa public key |
| `amount` | `string \| number` | Yes | Amount to send |
| `phoneNumber` | `string` | Yes | Recipient phone in `254...` format |
| `commandID` | `string` | No | `BusinessPayment` (default), `SalaryPayment`, or `PromotionPayment` |
| `remarks` | `string` | No | Transaction remarks |
| `occassion` | `string` | No | Occasion description |
| `initiatorID` | `string` | No | API initiator username (default: `"testapi"`) |
| `resultCallbackUrl` | `string` | No | Webhook for successful result |
| `timeoutCallbackUrl` | `string` | No | Webhook for timeout |

---

### `registerC2B`

Registers the confirmation and validation URLs that M-Pesa calls when a customer pays to your short code directly (e.g., via *Pay Bill* on their phone). Call this once when setting up your integration.

```ts
import { registerC2B } from 'mpesa-snap';

await registerC2B({
  consumer_key: 'YOUR_CONSUMER_KEY',
  consumer_secret: 'YOUR_CONSUMER_SECRET',
  environment: 'sandbox',
  short_code: '600000',
  response_type: 'Completed',       // or 'Cancelled'
  confirmation_url: 'https://yourapp.com/mpesa/c2b/confirm',
  validation_url: 'https://yourapp.com/mpesa/c2b/validate',
});
```

| Field | Type | Required | Description |
|---|---|---|---|
| `consumer_key` | `string` | Yes | App consumer key |
| `consumer_secret` | `string` | Yes | App consumer secret |
| `environment` | `"sandbox" \| "production"` | Yes | Target environment |
| `short_code` | `string` | Yes | Business short code |
| `response_type` | `"Completed" \| "Cancelled"` | Yes | Action taken if validation URL is unreachable |
| `confirmation_url` | `string` | Yes | URL M-Pesa calls after payment succeeds |
| `validation_url` | `string` | Yes | URL M-Pesa calls to approve or reject the payment |

**Validation URL** — M-Pesa calls this before processing. Respond with `{"ResultCode": 0, "ResultDesc": "Accepted"}` to approve or `{"ResultCode": 1, "ResultDesc": "Rejected"}` to decline.

**Confirmation URL** — M-Pesa calls this after the payment completes. Always respond with `{"ResultCode": 0, "ResultDesc": "Success"}`.

---

### `ratiba`

Creates a standing order (recurring payment) — also known as M-Pesa Ratiba. The customer is charged automatically at the set frequency without needing to enter their PIN each time.

```ts
import { ratiba } from 'mpesa-snap';

const response = await ratiba({
  consumer_key: 'YOUR_CONSUMER_KEY',
  consumer_secret: 'YOUR_CONSUMER_SECRET',
  environment: 'sandbox',
  orderName: 'Monthly Subscription',
  shortCode: '600000',
  amount: 1000,
  phoneNumber: '254712345678',
  resultUrl: 'https://yourapp.com/mpesa/ratiba/result',
  orderReference: 'SUB-001',
  transactionType: 'Standing Order Customer Pay Bill',
  frequency: 'Monthly',
  startDate: '2024-01-15',
  endDate: '2025-01-15',
  remarks: 'Monthly sub',   // optional, max 13 chars
});
```

**`frequency` options**

| Value | Billing cycle |
|---|---|
| `"One Off"` | Single charge on start date |
| `"Daily"` | Every day |
| `"Weekly"` | Every week |
| `"Monthly"` | Every month |
| `"Bi-Monthly"` | Every two months |
| `"Quarterly"` | Every three months |
| `"Half Yearly"` | Every six months |
| `"Yearly"` | Once a year |

**`transactionType` options**

| Value | Use for |
|---|---|
| `"Standing Order Customer Pay Bill"` | Paybill short codes |
| `"Standing Order Customer Pay Marchant"` | Till numbers |

---

### `auth`

Fetches an OAuth access token. Useful if you need the token for direct Safaricom API calls outside of this library.

```ts
import { auth } from 'mpesa-snap';

const { access_token } = await auth({
  consumer_key: 'YOUR_CONSUMER_KEY',
  consumer_secret: 'YOUR_CONSUMER_SECRET',
  environment: 'sandbox',
});

// use access_token in your own requests
```

---

## Handling Callbacks

M-Pesa sends results to your webhook URLs via HTTP POST. Here is what to expect and how to handle them.

### STK Push callback

```json
{
  "Body": {
    "stkCallback": {
      "MerchantRequestID": "29115-34620561-1",
      "CheckoutRequestID": "ws_CO_191220191020363925",
      "ResultCode": 0,
      "ResultDesc": "The service request is processed successfully.",
      "CallbackMetadata": {
        "Item": [
          { "Name": "Amount", "Value": 1 },
          { "Name": "MpesaReceiptNumber", "Value": "NLJ7RT61SV" },
          { "Name": "TransactionDate", "Value": 20191219102115 },
          { "Name": "PhoneNumber", "Value": 254708374149 }
        ]
      }
    }
  }
}
```

`ResultCode: 0` means success. Any other code means the payment failed or was cancelled by the user.

**Express.js example:**

```ts
app.post('/mpesa/callback', (req, res) => {
  const { stkCallback } = req.body.Body;

  if (stkCallback.ResultCode !== 0) {
    // payment failed or user cancelled
    console.log('Payment failed:', stkCallback.ResultDesc);
    return res.json({ ResultCode: 0, ResultDesc: 'Accepted' });
  }

  const items = stkCallback.CallbackMetadata.Item;
  const receipt = items.find(i => i.Name === 'MpesaReceiptNumber')?.Value;
  const amount = items.find(i => i.Name === 'Amount')?.Value;
  const phone = items.find(i => i.Name === 'PhoneNumber')?.Value;

  // update your database here
  console.log(`Payment received: KES ${amount} from ${phone}, receipt: ${receipt}`);

  res.json({ ResultCode: 0, ResultDesc: 'Success' });
});
```

### B2C result callback

```json
{
  "Result": {
    "ResultCode": 0,
    "ResultDesc": "The service request is processed successfully.",
    "OriginatorConversationID": "...",
    "ConversationID": "...",
    "TransactionID": "NLJ7RT61SV",
    "ResultParameters": {
      "ResultParameter": [
        { "Key": "TransactionAmount", "Value": 500 },
        { "Key": "TransactionReceipt", "Value": "NLJ7RT61SV" },
        { "Key": "ReceiverPartyPublicName", "Value": "254708374149 - John Doe" },
        { "Key": "TransactionCompletedDateTime", "Value": "19.12.2019 10:25:00" }
      ]
    }
  }
}
```

---

## Error Handling

All functions throw on network or API errors. Wrap calls in try/catch:

```ts
try {
  const response = await mpesa.stkPush({ ... });
  // handle success
} catch (error) {
  if (error.response) {
    // M-Pesa returned an error response
    console.error(error.response.status, error.response.data);
  } else {
    // network or configuration error
    console.error(error.message);
  }
}
```

Common M-Pesa error codes:

| Code | Meaning |
|---|---|
| `1` | Insufficient funds |
| `17` | M-Pesa service unavailable / limit reached |
| `1032` | Request cancelled by user |
| `1037` | Customer PIN timeout |
| `2001` | Wrong credentials |

---

## Going to Production

1. Change `environment` from `"sandbox"` to `"production"`.
2. Replace sandbox credentials with your live **Consumer Key**, **Consumer Secret**, and **Passkey** from the Safaricom portal.
3. Use your actual **Paybill** or **Till** number as `shortCode`.
4. Make sure your callback URLs are publicly reachable HTTPS endpoints.

```ts
const mpesa = new Mpesa(
  {
    consumer_key: process.env.MPESA_CONSUMER_KEY!,
    consumer_secret: process.env.MPESA_CONSUMER_SECRET!,
    shortCode: process.env.MPESA_SHORT_CODE!,
    password: process.env.MPESA_PASSKEY!,
  },
  'production' // <-- switch here
);
```

---

## License

ISC
