pesafy 0.5.2 → 0.5.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,12 +2,16 @@
2
2
 
3
3
  # pesafy 💳
4
4
 
5
- > **Type-safe M-PESA Daraja SDK** for Node.js, Bun, Deno, Cloudflare Workers, Next.js, Fastify, Hono, and Express.
5
+ > **Type-safe M-PESA Daraja SDK** for Node.js, Bun, Deno, Cloudflare Workers,
6
+ > Next.js, Fastify, Hono, and Express.
6
7
 
7
8
  [![npm version](https://img.shields.io/npm/v/pesafy.svg)](https://www.npmjs.com/package/pesafy)
8
9
  [![npm downloads](https://img.shields.io/npm/dm/pesafy.svg)](https://www.npmjs.com/package/pesafy)
9
10
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
10
11
  [![TypeScript](https://img.shields.io/badge/TypeScript-5.7-blue.svg)](https://www.typescriptlang.org/)
12
+ [![CI](https://github.com/levos-snr/pesafy/actions/workflows/ci.yml/badge.svg)](https://github.com/levos-snr/pesafy/actions/workflows/ci.yml)
13
+ [![codecov](https://codecov.io/github/levos-snr/pesafy/graph/badge.svg?token=JYK2BS1ZZF)](https://codecov.io/github/levos-snr/pesafy)
14
+ [![npm](https://img.shields.io/npm/v/pesafy)](https://www.npmjs.com/package/pesafy)
11
15
 
12
16
  ---
13
17
 
@@ -55,33 +59,34 @@ bun add pesafy # bun
55
59
  ## Quick Start
56
60
 
57
61
  ```typescript
58
- import { Mpesa } from "pesafy";
62
+ import { Mpesa } from 'pesafy'
59
63
 
60
64
  const mpesa = new Mpesa({
61
65
  consumerKey: process.env.MPESA_CONSUMER_KEY!,
62
66
  consumerSecret: process.env.MPESA_CONSUMER_SECRET!,
63
- environment: "sandbox", // "sandbox" | "production"
64
- lipaNaMpesaShortCode: "174379",
67
+ environment: 'sandbox', // "sandbox" | "production"
68
+ lipaNaMpesaShortCode: '174379',
65
69
  lipaNaMpesaPassKey: process.env.MPESA_PASSKEY!,
66
- });
70
+ })
67
71
 
68
72
  // Send an STK Push
69
73
  const response = await mpesa.stkPush({
70
74
  amount: 100,
71
- phoneNumber: "0712345678",
72
- callbackUrl: "https://yourdomain.com/api/mpesa/callback",
73
- accountReference: "INV-001",
74
- transactionDesc: "Payment",
75
- });
75
+ phoneNumber: '0712345678',
76
+ callbackUrl: 'https://yourdomain.com/api/mpesa/callback',
77
+ accountReference: 'INV-001',
78
+ transactionDesc: 'Payment',
79
+ })
76
80
 
77
- console.log(response.CheckoutRequestID);
81
+ console.log(response.CheckoutRequestID)
78
82
  ```
79
83
 
80
84
  ---
81
85
 
82
86
  ## CLI
83
87
 
84
- The pesafy CLI lets you interact with Daraja directly from your terminal — great for testing, debugging, and scripting.
88
+ The pesafy CLI lets you interact with Daraja directly from your terminal — great
89
+ for testing, debugging, and scripting.
85
90
 
86
91
  ### Setup
87
92
 
@@ -135,27 +140,27 @@ MPESA_QUEUE_TIMEOUT_URL
135
140
  ### Instantiating the client
136
141
 
137
142
  ```typescript
138
- import { Mpesa } from "pesafy";
143
+ import { Mpesa } from 'pesafy'
139
144
 
140
145
  const mpesa = new Mpesa({
141
- consumerKey: "...",
142
- consumerSecret: "...",
143
- environment: "sandbox",
146
+ consumerKey: '...',
147
+ consumerSecret: '...',
148
+ environment: 'sandbox',
144
149
 
145
150
  // STK Push
146
- lipaNaMpesaShortCode: "174379",
147
- lipaNaMpesaPassKey: "bfb279...",
151
+ lipaNaMpesaShortCode: '174379',
152
+ lipaNaMpesaPassKey: 'bfb279...',
148
153
 
149
154
  // Initiator-based APIs (B2C, Reversal, Balance, Tax)
150
- initiatorName: "testapi",
151
- initiatorPassword: "Safaricom123!",
152
- certificatePath: "./SandboxCertificate.cer",
155
+ initiatorName: 'testapi',
156
+ initiatorPassword: 'Safaricom123!',
157
+ certificatePath: './SandboxCertificate.cer',
153
158
 
154
159
  // HTTP tuning (optional)
155
160
  retries: 4, // default: 4
156
161
  retryDelay: 2000, // default: 2000 ms
157
162
  timeout: 30000, // default: 30 000 ms (per attempt)
158
- });
163
+ })
159
164
  ```
160
165
 
161
166
  ---
@@ -199,13 +204,17 @@ if (result.ok) {
199
204
  **Callback payload helpers:**
200
205
 
201
206
  ```typescript
202
- import { isStkCallbackSuccess, getCallbackValue, type StkPushCallback } from "pesafy";
207
+ import {
208
+ isStkCallbackSuccess,
209
+ getCallbackValue,
210
+ type StkPushCallback,
211
+ } from 'pesafy'
203
212
 
204
213
  function handleCallback(body: StkPushCallback) {
205
214
  if (isStkCallbackSuccess(body.Body.stkCallback)) {
206
- const receipt = getCallbackValue(body, "MpesaReceiptNumber"); // string
207
- const amount = getCallbackValue(body, "Amount"); // number
208
- const phone = getCallbackValue(body, "PhoneNumber"); // number
215
+ const receipt = getCallbackValue(body, 'MpesaReceiptNumber') // string
216
+ const amount = getCallbackValue(body, 'Amount') // number
217
+ const phone = getCallbackValue(body, 'PhoneNumber') // number
209
218
  }
210
219
  }
211
220
  ```
@@ -229,22 +238,22 @@ Register your Paybill or Till to receive M-PESA payments.
229
238
  ```typescript
230
239
  // 1. Register Confirmation + Validation URLs (do this once per shortcode)
231
240
  await mpesa.registerC2BUrls({
232
- shortCode: "600984",
233
- responseType: "Completed", // "Completed" | "Cancelled"
234
- confirmationUrl: "https://yourdomain.com/api/mpesa/c2b/confirmation",
235
- validationUrl: "https://yourdomain.com/api/mpesa/c2b/validation",
236
- apiVersion: "v2", // default — v2 masks MSISDN in callbacks
237
- });
241
+ shortCode: '600984',
242
+ responseType: 'Completed', // "Completed" | "Cancelled"
243
+ confirmationUrl: 'https://yourdomain.com/api/mpesa/c2b/confirmation',
244
+ validationUrl: 'https://yourdomain.com/api/mpesa/c2b/validation',
245
+ apiVersion: 'v2', // default — v2 masks MSISDN in callbacks
246
+ })
238
247
 
239
248
  // 2. Simulate (SANDBOX ONLY)
240
249
  await mpesa.simulateC2B({
241
- shortCode: "600984",
242
- commandId: "CustomerPayBillOnline", // or "CustomerBuyGoodsOnline"
250
+ shortCode: '600984',
251
+ commandId: 'CustomerPayBillOnline', // or "CustomerBuyGoodsOnline"
243
252
  amount: 10,
244
253
  msisdn: 254708374149,
245
- billRefNumber: "INV-001", // Paybill only — OMIT for Buy Goods
246
- apiVersion: "v2",
247
- });
254
+ billRefNumber: 'INV-001', // Paybill only — OMIT for Buy Goods
255
+ apiVersion: 'v2',
256
+ })
248
257
  ```
249
258
 
250
259
  **Validation webhook handlers:**
@@ -259,34 +268,34 @@ import {
259
268
  getC2BCustomerName,
260
269
  type C2BValidationPayload,
261
270
  type C2BConfirmationPayload,
262
- } from "pesafy";
271
+ } from 'pesafy'
263
272
 
264
273
  // Validation URL — must respond in ≤8 seconds
265
- app.post("/api/mpesa/c2b/validation", (req, res) => {
266
- const payload = req.body as C2BValidationPayload;
267
- const amount = getC2BAmount(payload);
274
+ app.post('/api/mpesa/c2b/validation', (req, res) => {
275
+ const payload = req.body as C2BValidationPayload
276
+ const amount = getC2BAmount(payload)
268
277
 
269
278
  if (amount > 100_000) {
270
279
  // Reject with specific error code
271
- return res.json(rejectC2BValidation("C2B00013")); // Invalid Amount
280
+ return res.json(rejectC2BValidation('C2B00013')) // Invalid Amount
272
281
  }
273
282
 
274
- res.json(acceptC2BValidation()); // Accept
283
+ res.json(acceptC2BValidation()) // Accept
275
284
  // or: res.json(acceptC2BValidation("MY-TX-ID")); // with correlation ID
276
- });
285
+ })
277
286
 
278
287
  // Confirmation URL — always respond 200 immediately
279
- app.post("/api/mpesa/c2b/confirmation", (req, res) => {
280
- const payload = req.body as C2BConfirmationPayload;
281
- const txId = getC2BTransactionId(payload);
282
- const amount = getC2BAmount(payload);
283
- const name = getC2BCustomerName(payload);
288
+ app.post('/api/mpesa/c2b/confirmation', (req, res) => {
289
+ const payload = req.body as C2BConfirmationPayload
290
+ const txId = getC2BTransactionId(payload)
291
+ const amount = getC2BAmount(payload)
292
+ const name = getC2BCustomerName(payload)
284
293
 
285
294
  // Process async
286
- processPayment({ txId, amount, name }).catch(console.error);
295
+ processPayment({ txId, amount, name }).catch(console.error)
287
296
 
288
- res.json({ ResultCode: 0, ResultDesc: "Success" });
289
- });
297
+ res.json({ ResultCode: 0, ResultDesc: 'Success' })
298
+ })
290
299
  ```
291
300
 
292
301
  **C2B Validation ResultCodes:**
@@ -310,27 +319,27 @@ Send money to customers or load funds to a B2C shortcode.
310
319
  ```typescript
311
320
  // BusinessPayToBulk — load funds to a B2C shortcode
312
321
  const ack = await mpesa.b2cPayment({
313
- commandId: "BusinessPayToBulk",
322
+ commandId: 'BusinessPayToBulk',
314
323
  amount: 50_000,
315
- partyA: "600979", // your MMF shortcode
316
- partyB: "600000", // target B2C shortcode
317
- accountReference: "BATCH-2024-01",
318
- resultUrl: "https://yourdomain.com/api/mpesa/b2c/result",
319
- queueTimeOutUrl: "https://yourdomain.com/api/mpesa/b2c/timeout",
320
- });
324
+ partyA: '600979', // your MMF shortcode
325
+ partyB: '600000', // target B2C shortcode
326
+ accountReference: 'BATCH-2024-01',
327
+ resultUrl: 'https://yourdomain.com/api/mpesa/b2c/result',
328
+ queueTimeOutUrl: 'https://yourdomain.com/api/mpesa/b2c/timeout',
329
+ })
321
330
 
322
331
  // BusinessPayment — direct payment to customer wallet
323
332
  await mpesa.b2cPayment({
324
- commandId: "BusinessPayment", // or "SalaryPayment" / "PromotionPayment"
333
+ commandId: 'BusinessPayment', // or "SalaryPayment" / "PromotionPayment"
325
334
  amount: 2500,
326
- partyA: "600979",
327
- partyB: "254712345678", // customer MSISDN
328
- accountReference: "SALARY-JAN",
329
- resultUrl: "https://yourdomain.com/api/mpesa/b2c/result",
330
- queueTimeOutUrl: "https://yourdomain.com/api/mpesa/b2c/timeout",
331
- remarks: "January salary",
332
- requester: "254712345678", // optional — consumer MSISDN
333
- });
335
+ partyA: '600979',
336
+ partyB: '254712345678', // customer MSISDN
337
+ accountReference: 'SALARY-JAN',
338
+ resultUrl: 'https://yourdomain.com/api/mpesa/b2c/result',
339
+ queueTimeOutUrl: 'https://yourdomain.com/api/mpesa/b2c/timeout',
340
+ remarks: 'January salary',
341
+ requester: '254712345678', // optional — consumer MSISDN
342
+ })
334
343
  ```
335
344
 
336
345
  **B2C Result webhook handler:**
@@ -346,22 +355,22 @@ import {
346
355
  getB2COriginatorConversationId,
347
356
  getB2CReceiverPublicName,
348
357
  getB2CDebitAccountBalance,
349
- } from "pesafy";
358
+ } from 'pesafy'
350
359
 
351
- app.post("/api/mpesa/b2c/result", (req, res) => {
352
- res.json({ ResultCode: 0, ResultDesc: "Accepted" }); // always respond 200 first
360
+ app.post('/api/mpesa/b2c/result', (req, res) => {
361
+ res.json({ ResultCode: 0, ResultDesc: 'Accepted' }) // always respond 200 first
353
362
 
354
- if (!isB2CResult(req.body)) return;
363
+ if (!isB2CResult(req.body)) return
355
364
 
356
365
  if (isB2CSuccess(req.body)) {
357
- const txId = getB2CTransactionId(req.body);
358
- const amount = getB2CAmount(req.body);
359
- const balance = getB2CDebitAccountBalance(req.body);
360
- console.log("B2C success:", { txId, amount, balance });
366
+ const txId = getB2CTransactionId(req.body)
367
+ const amount = getB2CAmount(req.body)
368
+ const balance = getB2CDebitAccountBalance(req.body)
369
+ console.log('B2C success:', { txId, amount, balance })
361
370
  } else if (isB2CFailure(req.body)) {
362
- console.error("B2C failed:", req.body.Result.ResultDesc);
371
+ console.error('B2C failed:', req.body.Result.ResultDesc)
363
372
  }
364
- });
373
+ })
365
374
  ```
366
375
 
367
376
  **B2C CommandIDs:**
@@ -381,14 +390,14 @@ Send a USSD Push to a merchant's till for B2B payments.
381
390
 
382
391
  ```typescript
383
392
  const ack = await mpesa.b2bExpressCheckout({
384
- primaryShortCode: "000001", // merchant till (debit party)
385
- receiverShortCode: "000002", // your Paybill (credit party)
393
+ primaryShortCode: '000001', // merchant till (debit party)
394
+ receiverShortCode: '000002', // your Paybill (credit party)
386
395
  amount: 5000,
387
- paymentRef: "INV-001",
388
- callbackUrl: "https://yourdomain.com/api/mpesa/b2b/callback",
389
- partnerName: "Acme Supplies",
390
- requestRefId: "unique-uuid-per-request", // auto-generated if omitted
391
- });
396
+ paymentRef: 'INV-001',
397
+ callbackUrl: 'https://yourdomain.com/api/mpesa/b2b/callback',
398
+ partnerName: 'Acme Supplies',
399
+ requestRefId: 'unique-uuid-per-request', // auto-generated if omitted
400
+ })
392
401
 
393
402
  // ack.code === "0" means USSD was initiated
394
403
  ```
@@ -403,21 +412,21 @@ import {
403
412
  getB2BTransactionId,
404
413
  getB2BAmount,
405
414
  getB2BConversationId,
406
- } from "pesafy";
415
+ } from 'pesafy'
407
416
 
408
- app.post("/api/mpesa/b2b/callback", (req, res) => {
409
- res.json({ ResultCode: 0, ResultDesc: "Accepted" });
417
+ app.post('/api/mpesa/b2b/callback', (req, res) => {
418
+ res.json({ ResultCode: 0, ResultDesc: 'Accepted' })
410
419
 
411
- if (!isB2BCheckoutCallback(req.body)) return;
420
+ if (!isB2BCheckoutCallback(req.body)) return
412
421
 
413
422
  if (isB2BCheckoutSuccess(req.body)) {
414
- const txId = getB2BTransactionId(req.body);
415
- const amount = getB2BAmount(req.body);
416
- console.log("B2B paid:", { txId, amount });
423
+ const txId = getB2BTransactionId(req.body)
424
+ const amount = getB2BAmount(req.body)
425
+ console.log('B2B paid:', { txId, amount })
417
426
  } else if (isB2BCheckoutCancelled(req.body)) {
418
- console.log("B2B cancelled by merchant");
427
+ console.log('B2B cancelled by merchant')
419
428
  }
420
- });
429
+ })
421
430
  ```
422
431
 
423
432
  **B2B Error codes:**
@@ -435,16 +444,17 @@ app.post("/api/mpesa/b2b/callback", (req, res) => {
435
444
 
436
445
  ### Account Balance
437
446
 
438
- Query the balance of your M-PESA shortcode. **Asynchronous** — result is POSTed to your `resultUrl`.
447
+ Query the balance of your M-PESA shortcode. **Asynchronous** — result is POSTed
448
+ to your `resultUrl`.
439
449
 
440
450
  ```typescript
441
451
  await mpesa.accountBalance({
442
- partyA: "174379",
443
- identifierType: "4", // "1"=MSISDN, "2"=Till, "4"=ShortCode
444
- resultUrl: "https://yourdomain.com/api/mpesa/balance/result",
445
- queueTimeOutUrl: "https://yourdomain.com/api/mpesa/balance/timeout",
446
- remarks: "Balance check",
447
- });
452
+ partyA: '174379',
453
+ identifierType: '4', // "1"=MSISDN, "2"=Till, "4"=ShortCode
454
+ resultUrl: 'https://yourdomain.com/api/mpesa/balance/result',
455
+ queueTimeOutUrl: 'https://yourdomain.com/api/mpesa/balance/timeout',
456
+ remarks: 'Balance check',
457
+ })
448
458
  ```
449
459
 
450
460
  **Parsing the result:**
@@ -455,22 +465,22 @@ import {
455
465
  parseAccountBalance,
456
466
  getAccountBalanceParam,
457
467
  type AccountBalanceResult,
458
- } from "pesafy";
468
+ } from 'pesafy'
459
469
 
460
- app.post("/api/mpesa/balance/result", (req, res) => {
461
- res.json({ ResultCode: 0, ResultDesc: "Accepted" });
470
+ app.post('/api/mpesa/balance/result', (req, res) => {
471
+ res.json({ ResultCode: 0, ResultDesc: 'Accepted' })
462
472
 
463
- const body = req.body as AccountBalanceResult;
464
- if (!isAccountBalanceSuccess(body)) return;
473
+ const body = req.body as AccountBalanceResult
474
+ if (!isAccountBalanceSuccess(body)) return
465
475
 
466
- const raw = getAccountBalanceParam(body, "AccountBalance") as string;
467
- const accounts = parseAccountBalance(raw ?? "");
476
+ const raw = getAccountBalanceParam(body, 'AccountBalance') as string
477
+ const accounts = parseAccountBalance(raw ?? '')
468
478
 
469
479
  for (const account of accounts) {
470
- console.log(`${account.name}: ${account.currency} ${account.amount}`);
480
+ console.log(`${account.name}: ${account.currency} ${account.amount}`)
471
481
  // e.g. "Working Account: KES 45000.00"
472
482
  }
473
- });
483
+ })
474
484
  ```
475
485
 
476
486
  ---
@@ -481,13 +491,13 @@ Query the result of any completed M-PESA transaction. **Asynchronous**.
481
491
 
482
492
  ```typescript
483
493
  await mpesa.transactionStatus({
484
- transactionId: "OEI2AK4XXXX",
485
- partyA: "174379",
486
- identifierType: "4",
487
- resultUrl: "https://yourdomain.com/api/mpesa/tx/result",
488
- queueTimeOutUrl: "https://yourdomain.com/api/mpesa/tx/timeout",
489
- remarks: "Check payment status",
490
- });
494
+ transactionId: 'OEI2AK4XXXX',
495
+ partyA: '174379',
496
+ identifierType: '4',
497
+ resultUrl: 'https://yourdomain.com/api/mpesa/tx/result',
498
+ queueTimeOutUrl: 'https://yourdomain.com/api/mpesa/tx/timeout',
499
+ remarks: 'Check payment status',
500
+ })
491
501
  ```
492
502
 
493
503
  ---
@@ -498,28 +508,28 @@ Reverse a completed M-PESA transaction. **Asynchronous**.
498
508
 
499
509
  ```typescript
500
510
  await mpesa.reverseTransaction({
501
- transactionId: "OEI2AK4XXXX",
502
- receiverParty: "174379",
503
- receiverIdentifierType: "4", // "1"=MSISDN, "2"=Till, "4"=ShortCode
511
+ transactionId: 'OEI2AK4XXXX',
512
+ receiverParty: '174379',
513
+ receiverIdentifierType: '4', // "1"=MSISDN, "2"=Till, "4"=ShortCode
504
514
  amount: 500,
505
- resultUrl: "https://yourdomain.com/api/mpesa/reversal/result",
506
- queueTimeOutUrl: "https://yourdomain.com/api/mpesa/reversal/timeout",
507
- remarks: "Erroneous charge",
508
- });
515
+ resultUrl: 'https://yourdomain.com/api/mpesa/reversal/result',
516
+ queueTimeOutUrl: 'https://yourdomain.com/api/mpesa/reversal/timeout',
517
+ remarks: 'Erroneous charge',
518
+ })
509
519
  ```
510
520
 
511
521
  **Reversal result handler:**
512
522
 
513
523
  ```typescript
514
- import { isReversalSuccess, getReversalTransactionId } from "pesafy";
524
+ import { isReversalSuccess, getReversalTransactionId } from 'pesafy'
515
525
 
516
- app.post("/api/mpesa/reversal/result", (req, res) => {
517
- res.json({ ResultCode: 0, ResultDesc: "Accepted" });
526
+ app.post('/api/mpesa/reversal/result', (req, res) => {
527
+ res.json({ ResultCode: 0, ResultDesc: 'Accepted' })
518
528
 
519
529
  if (isReversalSuccess(req.body)) {
520
- console.log("Reversed:", getReversalTransactionId(req.body));
530
+ console.log('Reversed:', getReversalTransactionId(req.body))
521
531
  }
522
- });
532
+ })
523
533
  ```
524
534
 
525
535
  ---
@@ -531,12 +541,12 @@ Remit tax to Kenya Revenue Authority via M-PESA. **Asynchronous**.
531
541
  ```typescript
532
542
  await mpesa.remitTax({
533
543
  amount: 5_000,
534
- partyA: "888880", // your business shortcode
535
- accountReference: "PRN1234XN", // KRA Payment Registration Number
536
- resultUrl: "https://yourdomain.com/api/mpesa/tax/result",
537
- queueTimeOutUrl: "https://yourdomain.com/api/mpesa/tax/timeout",
538
- remarks: "Monthly PAYE",
539
- });
544
+ partyA: '888880', // your business shortcode
545
+ accountReference: 'PRN1234XN', // KRA Payment Registration Number
546
+ resultUrl: 'https://yourdomain.com/api/mpesa/tax/result',
547
+ queueTimeOutUrl: 'https://yourdomain.com/api/mpesa/tax/timeout',
548
+ remarks: 'Monthly PAYE',
549
+ })
540
550
  // PartyB is always KRA_SHORTCODE ("572572") — auto-set
541
551
  ```
542
552
 
@@ -548,13 +558,13 @@ Generate an M-PESA QR code customers can scan to pay.
548
558
 
549
559
  ```typescript
550
560
  const qr = await mpesa.generateDynamicQR({
551
- merchantName: "My Shop",
552
- refNo: "INV-001",
561
+ merchantName: 'My Shop',
562
+ refNo: 'INV-001',
553
563
  amount: 500,
554
- trxCode: "BG", // "BG"=Buy Goods, "PB"=Paybill, "WA"=Withdraw, "SM"=Send Money
555
- cpi: "373132", // till / paybill / MSISDN
564
+ trxCode: 'BG', // "BG"=Buy Goods, "PB"=Paybill, "WA"=Withdraw, "SM"=Send Money
565
+ cpi: '373132', // till / paybill / MSISDN
556
566
  size: 300, // pixels (square)
557
- });
567
+ })
558
568
 
559
569
  // Render in HTML:
560
570
  // <img src={`data:image/png;base64,${qr.QRCode}`} />
@@ -621,52 +631,57 @@ await mpesa.cancelInvoice({ externalReference: "INV-001" });
621
631
  **IP verification** (Safaricom always calls from whitelisted IPs):
622
632
 
623
633
  ```typescript
624
- import { verifyWebhookIP, SAFARICOM_IPS } from "pesafy";
634
+ import { verifyWebhookIP, SAFARICOM_IPS } from 'pesafy'
625
635
 
626
- app.post("/api/mpesa/callback", (req, res) => {
627
- const ip = req.ip ?? req.headers["x-forwarded-for"];
636
+ app.post('/api/mpesa/callback', (req, res) => {
637
+ const ip = req.ip ?? req.headers['x-forwarded-for']
628
638
  if (!verifyWebhookIP(ip)) {
629
- console.warn("Callback from unknown IP:", ip);
639
+ console.warn('Callback from unknown IP:', ip)
630
640
  // Still return 200 — Safaricom will retry if you reject
631
641
  }
632
642
  // ... process
633
- res.json({ ResultCode: 0, ResultDesc: "Accepted" });
634
- });
643
+ res.json({ ResultCode: 0, ResultDesc: 'Accepted' })
644
+ })
635
645
  ```
636
646
 
637
647
  **Generic webhook handler:**
638
648
 
639
649
  ```typescript
640
- import { handleWebhook, isSuccessfulCallback, extractTransactionId, extractAmount } from "pesafy";
650
+ import {
651
+ handleWebhook,
652
+ isSuccessfulCallback,
653
+ extractTransactionId,
654
+ extractAmount,
655
+ } from 'pesafy'
641
656
 
642
- app.post("/api/mpesa/callback", (req, res) => {
657
+ app.post('/api/mpesa/callback', (req, res) => {
643
658
  const result = handleWebhook(req.body, {
644
659
  requestIP: req.ip,
645
660
  skipIPCheck: false, // set true in local dev
646
- });
661
+ })
647
662
 
648
663
  if (result.success && isSuccessfulCallback(result.data)) {
649
- const receipt = extractTransactionId(result.data);
650
- const amount = extractAmount(result.data);
664
+ const receipt = extractTransactionId(result.data)
665
+ const amount = extractAmount(result.data)
651
666
  }
652
667
 
653
- res.json({ ResultCode: 0, ResultDesc: "Accepted" });
654
- });
668
+ res.json({ ResultCode: 0, ResultDesc: 'Accepted' })
669
+ })
655
670
  ```
656
671
 
657
672
  **Retry with backoff** (for your own internal processing):
658
673
 
659
674
  ```typescript
660
- import { retryWithBackoff } from "pesafy";
675
+ import { retryWithBackoff } from 'pesafy'
661
676
 
662
677
  const outcome = await retryWithBackoff(() => saveToDatabase(webhookData), {
663
678
  maxRetries: 5,
664
679
  initialDelay: 500,
665
680
  maxDelay: 30_000,
666
- });
681
+ })
667
682
 
668
683
  if (!outcome.success) {
669
- console.error("Failed after", outcome.attempts, "attempts");
684
+ console.error('Failed after', outcome.attempts, 'attempts')
670
685
  }
671
686
  ```
672
687
 
@@ -677,64 +692,70 @@ if (!outcome.success) {
677
692
  ### Express Adapter
678
693
 
679
694
  ```typescript
680
- import express from "express";
695
+ import express from 'express'
681
696
  import {
682
697
  createMpesaExpressRouter,
683
698
  acceptC2BValidation,
684
699
  isB2CSuccess,
685
700
  getB2CTransactionId,
686
- } from "pesafy/express";
701
+ } from 'pesafy/express'
687
702
 
688
- const app = express();
689
- app.use(express.json());
703
+ const app = express()
704
+ app.use(express.json())
690
705
 
691
- const router = express.Router();
706
+ const router = express.Router()
692
707
 
693
708
  createMpesaExpressRouter(router, {
694
709
  consumerKey: process.env.MPESA_CONSUMER_KEY!,
695
710
  consumerSecret: process.env.MPESA_CONSUMER_SECRET!,
696
- environment: "sandbox",
697
- lipaNaMpesaShortCode: "174379",
711
+ environment: 'sandbox',
712
+ lipaNaMpesaShortCode: '174379',
698
713
  lipaNaMpesaPassKey: process.env.MPESA_PASSKEY!,
699
- callbackUrl: "https://yourdomain.com/api/mpesa/express/callback",
714
+ callbackUrl: 'https://yourdomain.com/api/mpesa/express/callback',
700
715
 
701
716
  // Initiator (for B2C, Reversal, Balance)
702
- initiatorName: "testapi",
703
- initiatorPassword: "Safaricom123!",
704
- certificatePath: "./SandboxCertificate.cer",
717
+ initiatorName: 'testapi',
718
+ initiatorPassword: 'Safaricom123!',
719
+ certificatePath: './SandboxCertificate.cer',
705
720
 
706
721
  // Result endpoints
707
- resultUrl: "https://yourdomain.com/api/mpesa/transaction-status/result",
708
- queueTimeOutUrl: "https://yourdomain.com/api/mpesa/timeout",
722
+ resultUrl: 'https://yourdomain.com/api/mpesa/transaction-status/result',
723
+ queueTimeOutUrl: 'https://yourdomain.com/api/mpesa/timeout',
709
724
 
710
725
  // C2B
711
- c2bShortCode: "600984",
712
- c2bConfirmationUrl: "https://yourdomain.com/api/mpesa/c2b/confirmation",
713
- c2bValidationUrl: "https://yourdomain.com/api/mpesa/c2b/validation",
726
+ c2bShortCode: '600984',
727
+ c2bConfirmationUrl: 'https://yourdomain.com/api/mpesa/c2b/confirmation',
728
+ c2bValidationUrl: 'https://yourdomain.com/api/mpesa/c2b/validation',
714
729
  onC2BValidation: async (payload) => {
715
- const amount = Number(payload.TransAmount);
716
- if (amount > 100_000) return { ResultCode: "C2B00013", ResultDesc: "Rejected" };
717
- return acceptC2BValidation();
730
+ const amount = Number(payload.TransAmount)
731
+ if (amount > 100_000)
732
+ return { ResultCode: 'C2B00013', ResultDesc: 'Rejected' }
733
+ return acceptC2BValidation()
718
734
  },
719
735
  onC2BConfirmation: async (payload) => {
720
- await db.payments.create({ txId: payload.TransID, amount: Number(payload.TransAmount) });
736
+ await db.payments.create({
737
+ txId: payload.TransID,
738
+ amount: Number(payload.TransAmount),
739
+ })
721
740
  },
722
741
 
723
742
  // B2C
724
- b2cPartyA: "600979",
725
- b2cResultUrl: "https://yourdomain.com/api/mpesa/b2c/result",
726
- b2cQueueTimeOutUrl: "https://yourdomain.com/api/mpesa/b2c/timeout",
743
+ b2cPartyA: '600979',
744
+ b2cResultUrl: 'https://yourdomain.com/api/mpesa/b2c/result',
745
+ b2cQueueTimeOutUrl: 'https://yourdomain.com/api/mpesa/b2c/timeout',
727
746
  onB2CResult: async (result) => {
728
747
  if (isB2CSuccess(result)) {
729
- await db.disbursements.markCompleted({ txId: getB2CTransactionId(result)! });
748
+ await db.disbursements.markCompleted({
749
+ txId: getB2CTransactionId(result)!,
750
+ })
730
751
  }
731
752
  },
732
753
 
733
754
  skipIPCheck: true, // local dev only
734
- });
755
+ })
735
756
 
736
- app.use("/api", router);
737
- app.listen(3000);
757
+ app.use('/api', router)
758
+ app.listen(3000)
738
759
  ```
739
760
 
740
761
  **Routes mounted by `createMpesaExpressRouter`:**
@@ -764,35 +785,35 @@ app.listen(3000);
764
785
  Works on Bun, Cloudflare Workers, Deno, and Node.js via Hono.
765
786
 
766
787
  ```typescript
767
- import { Hono } from "hono";
768
- import { createMpesaHonoRouter } from "pesafy/adapters/hono";
788
+ import { Hono } from 'hono'
789
+ import { createMpesaHonoRouter } from 'pesafy/adapters/hono'
769
790
 
770
- const app = new Hono();
791
+ const app = new Hono()
771
792
 
772
793
  createMpesaHonoRouter(app, {
773
794
  consumerKey: process.env.MPESA_CONSUMER_KEY!,
774
795
  consumerSecret: process.env.MPESA_CONSUMER_SECRET!,
775
- environment: "sandbox",
776
- lipaNaMpesaShortCode: "174379",
796
+ environment: 'sandbox',
797
+ lipaNaMpesaShortCode: '174379',
777
798
  lipaNaMpesaPassKey: process.env.MPESA_PASSKEY!,
778
- callbackUrl: "https://yourdomain.com/mpesa/express/callback",
779
- resultUrl: "https://yourdomain.com/mpesa/result",
780
- queueTimeOutUrl: "https://yourdomain.com/mpesa/timeout",
799
+ callbackUrl: 'https://yourdomain.com/mpesa/express/callback',
800
+ resultUrl: 'https://yourdomain.com/mpesa/result',
801
+ queueTimeOutUrl: 'https://yourdomain.com/mpesa/timeout',
781
802
 
782
803
  onStkSuccess: async ({ receiptNumber, amount, phone }) => {
783
- await db.payments.create({ receiptNumber, amount, phone });
804
+ await db.payments.create({ receiptNumber, amount, phone })
784
805
  },
785
806
  onStkFailure: ({ resultCode, resultDesc }) => {
786
- console.warn("STK failed:", resultCode, resultDesc);
807
+ console.warn('STK failed:', resultCode, resultDesc)
787
808
  },
788
809
  skipIPCheck: true,
789
- });
810
+ })
790
811
 
791
812
  // Bun
792
- export default app;
813
+ export default app
793
814
 
794
815
  // Cloudflare Workers
795
- export default { fetch: app.fetch };
816
+ export default { fetch: app.fetch }
796
817
  ```
797
818
 
798
819
  ---
@@ -801,48 +822,52 @@ export default { fetch: app.fetch };
801
822
 
802
823
  ```typescript
803
824
  // app/api/mpesa/stk-push/route.ts
804
- import { createStkPushHandler } from "pesafy/adapters/nextjs";
825
+ import { createStkPushHandler } from 'pesafy/adapters/nextjs'
805
826
 
806
827
  export const POST = createStkPushHandler({
807
828
  consumerKey: process.env.MPESA_CONSUMER_KEY!,
808
829
  consumerSecret: process.env.MPESA_CONSUMER_SECRET!,
809
- environment: "sandbox",
830
+ environment: 'sandbox',
810
831
  lipaNaMpesaShortCode: process.env.MPESA_SHORTCODE!,
811
832
  lipaNaMpesaPassKey: process.env.MPESA_PASSKEY!,
812
833
  callbackUrl: process.env.MPESA_CALLBACK_URL!,
813
- });
834
+ })
814
835
  ```
815
836
 
816
837
  ```typescript
817
838
  // app/api/mpesa/callback/route.ts
818
- import { createStkCallbackHandler } from "pesafy/adapters/nextjs";
839
+ import { createStkCallbackHandler } from 'pesafy/adapters/nextjs'
819
840
 
820
841
  export const POST = createStkCallbackHandler({
821
842
  consumerKey: process.env.MPESA_CONSUMER_KEY!,
822
843
  consumerSecret: process.env.MPESA_CONSUMER_SECRET!,
823
- environment: "sandbox",
844
+ environment: 'sandbox',
824
845
  callbackUrl: process.env.MPESA_CALLBACK_URL!,
825
846
  onSuccess: async ({ receiptNumber, amount, phone }) => {
826
- await db.payments.create({ receiptNumber, amount: amount ?? 0, phone: phone ?? "" });
847
+ await db.payments.create({
848
+ receiptNumber,
849
+ amount: amount ?? 0,
850
+ phone: phone ?? '',
851
+ })
827
852
  },
828
853
  onFailure: ({ resultCode, resultDesc }) => {
829
- console.warn("Payment failed:", resultCode, resultDesc);
854
+ console.warn('Payment failed:', resultCode, resultDesc)
830
855
  },
831
- });
856
+ })
832
857
  ```
833
858
 
834
859
  ```typescript
835
860
  // app/api/mpesa/stk-query/route.ts
836
- import { createStkQueryHandler } from "pesafy/adapters/nextjs";
861
+ import { createStkQueryHandler } from 'pesafy/adapters/nextjs'
837
862
 
838
863
  export const POST = createStkQueryHandler({
839
864
  consumerKey: process.env.MPESA_CONSUMER_KEY!,
840
865
  consumerSecret: process.env.MPESA_CONSUMER_SECRET!,
841
- environment: "sandbox",
866
+ environment: 'sandbox',
842
867
  lipaNaMpesaShortCode: process.env.MPESA_SHORTCODE!,
843
868
  lipaNaMpesaPassKey: process.env.MPESA_PASSKEY!,
844
869
  callbackUrl: process.env.MPESA_CALLBACK_URL!,
845
- });
870
+ })
846
871
  ```
847
872
 
848
873
  ---
@@ -850,34 +875,35 @@ export const POST = createStkQueryHandler({
850
875
  ### Fastify Adapter
851
876
 
852
877
  ```typescript
853
- import Fastify from "fastify";
854
- import { registerMpesaRoutes } from "pesafy/adapters/fastify";
878
+ import Fastify from 'fastify'
879
+ import { registerMpesaRoutes } from 'pesafy/adapters/fastify'
855
880
 
856
- const app = Fastify({ logger: true });
881
+ const app = Fastify({ logger: true })
857
882
 
858
883
  await registerMpesaRoutes(app, {
859
884
  consumerKey: process.env.MPESA_CONSUMER_KEY!,
860
885
  consumerSecret: process.env.MPESA_CONSUMER_SECRET!,
861
- environment: "sandbox",
862
- lipaNaMpesaShortCode: "174379",
886
+ environment: 'sandbox',
887
+ lipaNaMpesaShortCode: '174379',
863
888
  lipaNaMpesaPassKey: process.env.MPESA_PASSKEY!,
864
- callbackUrl: "https://yourdomain.com/mpesa/callback",
865
- resultUrl: "https://yourdomain.com/mpesa/result",
866
- queueTimeOutUrl: "https://yourdomain.com/mpesa/timeout",
889
+ callbackUrl: 'https://yourdomain.com/mpesa/callback',
890
+ resultUrl: 'https://yourdomain.com/mpesa/result',
891
+ queueTimeOutUrl: 'https://yourdomain.com/mpesa/timeout',
867
892
  skipIPCheck: true,
868
893
  onStkSuccess: async ({ receiptNumber, amount, phone }) => {
869
- app.log.info({ receiptNumber, amount, phone }, "Payment received");
894
+ app.log.info({ receiptNumber, amount, phone }, 'Payment received')
870
895
  },
871
- });
896
+ })
872
897
 
873
- await app.listen({ port: 3000 });
898
+ await app.listen({ port: 3000 })
874
899
  ```
875
900
 
876
901
  ---
877
902
 
878
903
  ## Branded Types
879
904
 
880
- pesafy ships opt-in branded primitives that catch type bugs at compile time — not at runtime.
905
+ pesafy ships opt-in branded primitives that catch type bugs at compile time —
906
+ not at runtime.
881
907
 
882
908
  ```typescript
883
909
  import {
@@ -887,11 +913,11 @@ import {
887
913
  type KesAmount,
888
914
  type MsisdnKE,
889
915
  type PaybillCode,
890
- } from "pesafy";
916
+ } from 'pesafy'
891
917
 
892
- const amount: KesAmount = toKesAmount(100); // throws if < 1 or fractional
893
- const phone: MsisdnKE = toMsisdn("0712345678"); // throws if unparseable
894
- const code: PaybillCode = toPaybill("174379");
918
+ const amount: KesAmount = toKesAmount(100) // throws if < 1 or fractional
919
+ const phone: MsisdnKE = toMsisdn('0712345678') // throws if unparseable
920
+ const code: PaybillCode = toPaybill('174379')
895
921
 
896
922
  // ✅ Safe — editor shows exact types
897
923
  // ❌ Compile error — can't pass plain number where KesAmount is expected
@@ -962,22 +988,22 @@ try {
962
988
  ### Phone number formatting
963
989
 
964
990
  ```typescript
965
- import { formatSafaricomPhone } from "pesafy";
991
+ import { formatSafaricomPhone } from 'pesafy'
966
992
 
967
- formatSafaricomPhone("0712345678"); // → "254712345678"
968
- formatSafaricomPhone("+254712345678"); // → "254712345678"
969
- formatSafaricomPhone("712345678"); // → "254712345678"
970
- formatSafaricomPhone("254712345678"); // → "254712345678"
993
+ formatSafaricomPhone('0712345678') // → "254712345678"
994
+ formatSafaricomPhone('+254712345678') // → "254712345678"
995
+ formatSafaricomPhone('712345678') // → "254712345678"
996
+ formatSafaricomPhone('254712345678') // → "254712345678"
971
997
  ```
972
998
 
973
999
  ### Security credential encryption
974
1000
 
975
1001
  ```typescript
976
- import { encryptSecurityCredential } from "pesafy";
977
- import { readFileSync } from "fs";
1002
+ import { encryptSecurityCredential } from 'pesafy'
1003
+ import { readFileSync } from 'fs'
978
1004
 
979
- const pem = readFileSync("./SandboxCertificate.cer", "utf-8");
980
- const credential = encryptSecurityCredential("Safaricom123!", pem);
1005
+ const pem = readFileSync('./SandboxCertificate.cer', 'utf-8')
1006
+ const credential = encryptSecurityCredential('Safaricom123!', pem)
981
1007
  // Pass as config.securityCredential to skip per-call encryption
982
1008
  ```
983
1009