pesafy 0.4.2 β†’ 0.5.0

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.
Files changed (58) hide show
  1. package/CHANGELOG.md +171 -0
  2. package/README.md +981 -108
  3. package/dist/adapters/fastify.cjs +1607 -0
  4. package/dist/adapters/fastify.cjs.map +1 -0
  5. package/dist/adapters/fastify.d.cts +49 -0
  6. package/dist/adapters/fastify.d.mts +49 -0
  7. package/dist/adapters/fastify.mjs +1606 -0
  8. package/dist/adapters/fastify.mjs.map +1 -0
  9. package/dist/adapters/hono.cjs +1651 -0
  10. package/dist/adapters/hono.cjs.map +1 -0
  11. package/dist/adapters/hono.d.cts +55 -0
  12. package/dist/adapters/hono.d.mts +55 -0
  13. package/dist/adapters/hono.mjs +1650 -0
  14. package/dist/adapters/hono.mjs.map +1 -0
  15. package/dist/adapters/nextjs.cjs +1655 -0
  16. package/dist/adapters/nextjs.cjs.map +1 -0
  17. package/dist/adapters/nextjs.d.cts +79 -0
  18. package/dist/adapters/nextjs.d.mts +79 -0
  19. package/dist/adapters/nextjs.mjs +1651 -0
  20. package/dist/adapters/nextjs.mjs.map +1 -0
  21. package/dist/cli/encryption-BA-_xrIW.mjs +45 -0
  22. package/dist/cli/encryption-CkSveeYj.cjs +45 -0
  23. package/dist/cli/errors-Bscvlb7X.cjs +45 -0
  24. package/dist/cli/errors-DL4bkMZV.mjs +40 -0
  25. package/dist/cli/index.cjs +559 -0
  26. package/dist/cli/index.mjs +560 -0
  27. package/dist/cli/phone-5wwAaQ_8.mjs +21 -0
  28. package/dist/cli/phone-BD4QmEyl.cjs +21 -0
  29. package/dist/cli/utils-BzEKV3nJ.mjs +30 -0
  30. package/dist/cli/utils-Dg9Gv_D3.cjs +31 -0
  31. package/dist/components/react/index.cjs +0 -19
  32. package/dist/components/react/index.d.cts +1 -2
  33. package/dist/components/react/index.d.mts +1 -0
  34. package/dist/components/react/index.mjs +1 -0
  35. package/dist/express/index.cjs +2201 -0
  36. package/dist/express/index.cjs.map +1 -0
  37. package/dist/express/index.d.cts +1322 -0
  38. package/dist/express/index.d.mts +1322 -0
  39. package/dist/express/index.mjs +2199 -0
  40. package/dist/express/index.mjs.map +1 -0
  41. package/dist/index.cjs +1961 -1088
  42. package/dist/index.cjs.map +1 -1
  43. package/dist/index.d.cts +1477 -1071
  44. package/dist/index.d.mts +1907 -0
  45. package/dist/index.mjs +2050 -0
  46. package/dist/index.mjs.map +1 -0
  47. package/package.json +100 -74
  48. package/dist/components/react/index.cjs.map +0 -1
  49. package/dist/components/react/index.d.ts +0 -2
  50. package/dist/components/react/index.js +0 -1
  51. package/dist/components/react/index.js.map +0 -1
  52. package/dist/components/react/styles.css +0 -2
  53. package/dist/components/react/styles.css.map +0 -1
  54. package/dist/components/react/styles.d.cts +0 -2
  55. package/dist/components/react/styles.d.ts +0 -2
  56. package/dist/index.d.ts +0 -1501
  57. package/dist/index.js +0 -1209
  58. package/dist/index.js.map +0 -1
package/README.md CHANGED
@@ -1,177 +1,1050 @@
1
- # Pesafy πŸ’³
1
+ <!-- πŸ“ PATH: README.md -->
2
2
 
3
- > A powerful, type-safe payment gateway library for African payment systems, starting with M-Pesa Daraja API
3
+ # pesafy πŸ’³
4
+
5
+ > **Type-safe M-PESA Daraja SDK** for Node.js, Bun, Deno, Cloudflare Workers, Next.js, Fastify, Hono, and Express.
4
6
 
5
7
  [![npm version](https://img.shields.io/npm/v/pesafy.svg)](https://www.npmjs.com/package/pesafy)
8
+ [![npm downloads](https://img.shields.io/npm/dm/pesafy.svg)](https://www.npmjs.com/package/pesafy)
6
9
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
7
10
  [![TypeScript](https://img.shields.io/badge/TypeScript-5.7-blue.svg)](https://www.typescriptlang.org/)
8
11
 
9
- Pesafy simplifies payment processing for Kenyan merchants by providing a clean, well-documented, and easy-to-use interface for M-Pesa transactions. Built with TypeScript, following industry best practices inspired by Stripe.
12
+ ---
10
13
 
11
- ## ✨ Features
14
+ ## Table of Contents
12
15
 
13
- - πŸš€ **Easy Integration**: Simple, intuitive API design
14
- - πŸ”’ **Type-Safe**: Full TypeScript support with comprehensive types
15
- - 🎯 **Complete M-Pesa Coverage**: STK Push, B2C, B2B, C2B, QR Codes, and more
16
- - πŸ” **Secure**: Built-in security credential encryption and webhook verification
17
- - 🎨 **Dashboard Ready**: Payment monitoring and webhook management dashboard
18
- - 🧩 **Component Library**: Reusable payment components for React/Vue
19
- - πŸ“š **Well Documented**: Comprehensive documentation and examples
20
- - ⚑ **Fast**: Built with Bun for optimal performance
16
+ - [Installation](#installation)
17
+ - [Quick Start](#quick-start)
18
+ - [CLI](#cli)
19
+ - [API Reference](#api-reference)
20
+ - [STK Push (M-PESA Express)](#stk-push-m-pesa-express)
21
+ - [C2B (Customer to Business)](#c2b-customer-to-business)
22
+ - [B2C (Business to Customer)](#b2c-business-to-customer)
23
+ - [B2B Express Checkout](#b2b-express-checkout)
24
+ - [Account Balance](#account-balance)
25
+ - [Transaction Status](#transaction-status)
26
+ - [Transaction Reversal](#transaction-reversal)
27
+ - [Tax Remittance (KRA)](#tax-remittance-kra)
28
+ - [Dynamic QR Code](#dynamic-qr-code)
29
+ - [Bill Manager](#bill-manager)
30
+ - [Webhooks](#webhooks)
31
+ - [Framework Adapters](#framework-adapters)
32
+ - [Express](#express-adapter)
33
+ - [Hono (Bun / Cloudflare Workers)](#hono-adapter)
34
+ - [Next.js App Router](#nextjs-adapter)
35
+ - [Fastify](#fastify-adapter)
36
+ - [Branded Types](#branded-types)
37
+ - [Error Handling](#error-handling)
38
+ - [Utilities](#utilities)
39
+ - [Configuration Reference](#configuration-reference)
40
+ - [Roadmap](#roadmap)
21
41
 
22
- ## πŸ“¦ Installation
42
+ ---
43
+
44
+ ## Installation
23
45
 
24
46
  ```bash
25
- # Using npm
26
- npm install pesafy
47
+ npm install pesafy # npm
48
+ yarn add pesafy # yarn
49
+ pnpm add pesafy # pnpm
50
+ bun add pesafy # bun
51
+ ```
52
+
53
+ ---
54
+
55
+ ## Quick Start
56
+
57
+ ```typescript
58
+ import { Mpesa } from "pesafy";
27
59
 
28
- # Using yarn
29
- yarn add pesafy
60
+ const mpesa = new Mpesa({
61
+ consumerKey: process.env.MPESA_CONSUMER_KEY!,
62
+ consumerSecret: process.env.MPESA_CONSUMER_SECRET!,
63
+ environment: "sandbox", // "sandbox" | "production"
64
+ lipaNaMpesaShortCode: "174379",
65
+ lipaNaMpesaPassKey: process.env.MPESA_PASSKEY!,
66
+ });
30
67
 
31
- # Using pnpm
32
- pnpm add pesafy
68
+ // Send an STK Push
69
+ const response = await mpesa.stkPush({
70
+ amount: 100,
71
+ phoneNumber: "0712345678",
72
+ callbackUrl: "https://yourdomain.com/api/mpesa/callback",
73
+ accountReference: "INV-001",
74
+ transactionDesc: "Payment",
75
+ });
33
76
 
34
- # Using bun
35
- bun add pesafy
77
+ console.log(response.CheckoutRequestID);
36
78
  ```
37
79
 
38
- ## πŸš€ Quick Start
80
+ ---
81
+
82
+ ## CLI
83
+
84
+ The pesafy CLI lets you interact with Daraja directly from your terminal β€” great for testing, debugging, and scripting.
85
+
86
+ ### Setup
87
+
88
+ ```bash
89
+ # Interactive setup β€” creates .env in your project
90
+ npx pesafy init
91
+
92
+ # Validate your .env config
93
+ npx pesafy doctor
94
+ ```
95
+
96
+ ### Commands
97
+
98
+ ```
99
+ npx pesafy init β€” Scaffold .env interactively
100
+ npx pesafy doctor β€” Validate .env for common mistakes
101
+ npx pesafy token β€” Print a fresh OAuth token
102
+ npx pesafy encrypt β€” Encrypt initiator password β†’ SecurityCredential
103
+ npx pesafy validate-phone <phone> β€” Validate / normalise a Kenyan phone number
104
+ npx pesafy stk-push β€” Initiate an STK Push (interactive prompts)
105
+ npx pesafy stk-push --amount 100 --phone 0712345678 --ref INV-001
106
+ npx pesafy stk-query <checkoutId> β€” Check STK Push status
107
+ npx pesafy balance β€” Query M-PESA account balance
108
+ npx pesafy reversal <txId> β€” Initiate a transaction reversal
109
+ npx pesafy register-c2b-urls β€” Register C2B Confirmation + Validation URLs
110
+ npx pesafy simulate-c2b β€” Simulate a C2B payment (sandbox only)
111
+ npx pesafy version β€” Print library version
112
+ npx pesafy help β€” Show help
113
+ ```
114
+
115
+ ### Environment variables read by the CLI
116
+
117
+ ```bash
118
+ MPESA_CONSUMER_KEY
119
+ MPESA_CONSUMER_SECRET
120
+ MPESA_ENVIRONMENT # sandbox | production
121
+ MPESA_SHORTCODE
122
+ MPESA_PASSKEY
123
+ MPESA_CALLBACK_URL
124
+ MPESA_INITIATOR_NAME
125
+ MPESA_INITIATOR_PASSWORD
126
+ MPESA_CERTIFICATE_PATH # path to .cer file
127
+ MPESA_RESULT_URL
128
+ MPESA_QUEUE_TIMEOUT_URL
129
+ ```
130
+
131
+ ---
132
+
133
+ ## API Reference
134
+
135
+ ### Instantiating the client
39
136
 
40
137
  ```typescript
41
138
  import { Mpesa } from "pesafy";
42
139
 
43
140
  const mpesa = new Mpesa({
44
- consumerKey: "your-consumer-key",
45
- consumerSecret: "your-consumer-secret",
141
+ consumerKey: "...",
142
+ consumerSecret: "...",
46
143
  environment: "sandbox",
144
+
145
+ // STK Push
47
146
  lipaNaMpesaShortCode: "174379",
48
- lipaNaMpesaPassKey: "your-passkey",
147
+ lipaNaMpesaPassKey: "bfb279...",
148
+
149
+ // Initiator-based APIs (B2C, Reversal, Balance, Tax)
150
+ initiatorName: "testapi",
151
+ initiatorPassword: "Safaricom123!",
152
+ certificatePath: "./SandboxCertificate.cer",
153
+
154
+ // HTTP tuning (optional)
155
+ retries: 4, // default: 4
156
+ retryDelay: 2000, // default: 2000 ms
157
+ timeout: 30000, // default: 30 000 ms (per attempt)
49
158
  });
159
+ ```
50
160
 
51
- // STK Push (M-Pesa Express)
52
- const result = await mpesa.stkPush({
53
- amount: 100,
54
- phoneNumber: "254712345678",
55
- callbackUrl: "https://yoursite.com/callback",
56
- accountReference: "ORDER-123",
57
- transactionDesc: "Payment for order",
161
+ ---
162
+
163
+ ### STK Push (M-PESA Express)
164
+
165
+ Prompts the customer to enter their M-PESA PIN on their phone.
166
+
167
+ ```typescript
168
+ // Initiate
169
+ const push = await mpesa.stkPush({
170
+ amount: 100, // KES β€” whole numbers only
171
+ phoneNumber: "0712345678", // any Kenyan format
172
+ callbackUrl: "https://yourdomain.com/api/mpesa/callback",
173
+ accountReference: "INV-001", // max 12 chars
174
+ transactionDesc: "Subscription", // max 13 chars
175
+ transactionType: "CustomerPayBillOnline", // default β€” or "CustomerBuyGoodsOnline"
176
+ partyB: "174379", // defaults to shortCode; set till number for Buy Goods
177
+ });
178
+
179
+ console.log(push.CheckoutRequestID); // save this to query later
180
+
181
+ // Query status
182
+ const status = await mpesa.stkQuery({
183
+ checkoutRequestId: push.CheckoutRequestID,
58
184
  });
185
+
186
+ if (status.ResultCode === 0) {
187
+ console.log("Payment confirmed!");
188
+ }
189
+
190
+ // Safe variant β€” returns Result<T> instead of throwing
191
+ const result = await mpesa.stkPushSafe({ amount: 100, phoneNumber: "0712345678", ... });
192
+ if (result.ok) {
193
+ console.log(result.data.CheckoutRequestID);
194
+ } else {
195
+ console.error(result.error.code, result.error.message);
196
+ }
59
197
  ```
60
198
 
61
- ## πŸ“š Documentation
199
+ **Callback payload helpers:**
62
200
 
63
- - **[Getting Started Guide](./docs/guides/getting-started.md)** - Learn how to set up Pesafy
64
- - **[API Reference](./docs/api/)** - Complete API documentation
65
- - **[Examples](./docs/examples/)** - Code examples for different frameworks
66
- - **[Architecture](./ARCHITECTURE.md)** - System architecture overview
67
- - **[Project Plan](./PROJECT_PLAN.md)** - Development roadmap
201
+ ```typescript
202
+ import { isStkCallbackSuccess, getCallbackValue, type StkPushCallback } from "pesafy";
203
+
204
+ function handleCallback(body: StkPushCallback) {
205
+ 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
209
+ }
210
+ }
211
+ ```
68
212
 
69
- ## 🎯 Supported APIs
213
+ **STK Push ResultCodes:**
70
214
 
71
- ### βœ… Available
215
+ | Code | Meaning |
216
+ | ---- | ------------------------ |
217
+ | 0 | Success |
218
+ | 1 | Insufficient balance |
219
+ | 1032 | Cancelled by user |
220
+ | 1037 | DS timeout / unreachable |
221
+ | 2001 | Wrong PIN |
72
222
 
73
- - **STK Push (M-Pesa Express)** - Initiate payments via STK Push
74
- - **STK Query** - Check STK Push transaction status
75
- - **B2C** - Business to Customer payments
76
- - **B2B** - Business to Business payments
77
- - **C2B** - Register URLs & simulate (sandbox)
78
- - **Dynamic QR Codes** - Generate LIPA NA M-PESA QR codes
79
- - **Transaction Status** - Query transaction status
80
- - **Reversal** - Reverse transactions
223
+ ---
81
224
 
82
- ### πŸ“¦ Components
225
+ ### C2B (Customer to Business)
83
226
 
84
- - **PaymentButton** - Simple button to trigger payments
85
- - **PaymentForm** - Complete form for collecting payment details
86
- - **QRCode** - Display M-Pesa dynamic QR codes
87
- - **PaymentStatus** - Show payment status with visual feedback
227
+ Register your Paybill or Till to receive M-PESA payments.
88
228
 
89
- ## πŸ—οΈ Project Structure
229
+ ```typescript
230
+ // 1. Register Confirmation + Validation URLs (do this once per shortcode)
231
+ 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
+ });
90
238
 
239
+ // 2. Simulate (SANDBOX ONLY)
240
+ await mpesa.simulateC2B({
241
+ shortCode: "600984",
242
+ commandId: "CustomerPayBillOnline", // or "CustomerBuyGoodsOnline"
243
+ amount: 10,
244
+ msisdn: 254708374149,
245
+ billRefNumber: "INV-001", // Paybill only β€” OMIT for Buy Goods
246
+ apiVersion: "v2",
247
+ });
91
248
  ```
92
- pesafy/
93
- β”œβ”€β”€ src/ # Source code
94
- β”œβ”€β”€ docs/ # Documentation
95
- β”œβ”€β”€ tests/ # Test files
96
- β”œβ”€β”€ examples/ # Example projects
97
- └── components/ # Payment components (coming soon)
249
+
250
+ **Validation webhook handlers:**
251
+
252
+ ```typescript
253
+ import {
254
+ acceptC2BValidation,
255
+ rejectC2BValidation,
256
+ isC2BPayload,
257
+ getC2BAmount,
258
+ getC2BTransactionId,
259
+ getC2BCustomerName,
260
+ type C2BValidationPayload,
261
+ type C2BConfirmationPayload,
262
+ } from "pesafy";
263
+
264
+ // 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);
268
+
269
+ if (amount > 100_000) {
270
+ // Reject with specific error code
271
+ return res.json(rejectC2BValidation("C2B00013")); // Invalid Amount
272
+ }
273
+
274
+ res.json(acceptC2BValidation()); // Accept
275
+ // or: res.json(acceptC2BValidation("MY-TX-ID")); // with correlation ID
276
+ });
277
+
278
+ // 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);
284
+
285
+ // Process async
286
+ processPayment({ txId, amount, name }).catch(console.error);
287
+
288
+ res.json({ ResultCode: 0, ResultDesc: "Success" });
289
+ });
98
290
  ```
99
291
 
100
- See [FOLDER_STRUCTURE.md](./FOLDER_STRUCTURE.md) for detailed structure.
292
+ **C2B Validation ResultCodes:**
101
293
 
102
- ## πŸ§ͺ Development
294
+ | Code | Meaning |
295
+ | -------- | ---------------------- |
296
+ | 0 | Accept |
297
+ | C2B00011 | Invalid MSISDN |
298
+ | C2B00012 | Invalid Account Number |
299
+ | C2B00013 | Invalid Amount |
300
+ | C2B00014 | Invalid KYC Details |
301
+ | C2B00015 | Invalid ShortCode |
302
+ | C2B00016 | Other Error |
103
303
 
104
- ```bash
105
- # Install dependencies
106
- bun install
304
+ ---
305
+
306
+ ### B2C (Business to Customer)
307
+
308
+ Send money to customers or load funds to a B2C shortcode.
309
+
310
+ ```typescript
311
+ // BusinessPayToBulk β€” load funds to a B2C shortcode
312
+ const ack = await mpesa.b2cPayment({
313
+ commandId: "BusinessPayToBulk",
314
+ 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
+ });
321
+
322
+ // BusinessPayment β€” direct payment to customer wallet
323
+ await mpesa.b2cPayment({
324
+ commandId: "BusinessPayment", // or "SalaryPayment" / "PromotionPayment"
325
+ 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
+ });
334
+ ```
107
335
 
108
- # Run tests
109
- bun test
336
+ **B2C Result webhook handler:**
110
337
 
111
- # Build the library
112
- bun run build
338
+ ```typescript
339
+ import {
340
+ isB2CResult,
341
+ isB2CSuccess,
342
+ isB2CFailure,
343
+ getB2CTransactionId,
344
+ getB2CAmount,
345
+ getB2CConversationId,
346
+ getB2COriginatorConversationId,
347
+ getB2CReceiverPublicName,
348
+ getB2CDebitAccountBalance,
349
+ } from "pesafy";
113
350
 
114
- # Run type checking
115
- bun run typecheck
351
+ app.post("/api/mpesa/b2c/result", (req, res) => {
352
+ res.json({ ResultCode: 0, ResultDesc: "Accepted" }); // always respond 200 first
116
353
 
117
- # Format code
118
- bun run format
354
+ if (!isB2CResult(req.body)) return;
119
355
 
120
- # Lint code
121
- bun run lint
356
+ 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 });
361
+ } else if (isB2CFailure(req.body)) {
362
+ console.error("B2C failed:", req.body.Result.ResultDesc);
363
+ }
364
+ });
122
365
  ```
123
366
 
124
- ## πŸ“Š Dashboard (SaaS)
367
+ **B2C CommandIDs:**
125
368
 
126
- Run the payment monitoring dashboard:
369
+ | CommandID | Use Case |
370
+ | ------------------- | ----------------------------- |
371
+ | `BusinessPayToBulk` | Load funds to a B2C shortcode |
372
+ | `BusinessPayment` | Unsecured payment to customer |
373
+ | `SalaryPayment` | Salary disbursement |
374
+ | `PromotionPayment` | Promotions / bonus |
127
375
 
128
- ```bash
129
- cd dashboard
130
- bun install
131
- bun run dev
376
+ ---
377
+
378
+ ### B2B Express Checkout
379
+
380
+ Send a USSD Push to a merchant's till for B2B payments.
381
+
382
+ ```typescript
383
+ const ack = await mpesa.b2bExpressCheckout({
384
+ primaryShortCode: "000001", // merchant till (debit party)
385
+ receiverShortCode: "000002", // your Paybill (credit party)
386
+ 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
+ });
392
+
393
+ // ack.code === "0" means USSD was initiated
132
394
  ```
133
395
 
134
- Open http://localhost:3000 to view the dashboard. See [RUNNING.md](./RUNNING.md) for full instructions.
396
+ **B2B Callback handler:**
135
397
 
136
- ## πŸ“ Contributing
398
+ ```typescript
399
+ import {
400
+ isB2BCheckoutCallback,
401
+ isB2BCheckoutSuccess,
402
+ isB2BCheckoutCancelled,
403
+ getB2BTransactionId,
404
+ getB2BAmount,
405
+ getB2BConversationId,
406
+ } from "pesafy";
137
407
 
138
- Contributions are welcome! Please read our contributing guidelines before submitting PRs.
408
+ app.post("/api/mpesa/b2b/callback", (req, res) => {
409
+ res.json({ ResultCode: 0, ResultDesc: "Accepted" });
139
410
 
140
- 1. Fork the repository
141
- 2. Create your feature branch (`git checkout -b feature/amazing-feature`)
142
- 3. Commit your changes (`git commit -m 'Add some amazing feature'`)
143
- 4. Push to the branch (`git push origin feature/amazing-feature`)
144
- 5. Open a Pull Request
411
+ if (!isB2BCheckoutCallback(req.body)) return;
412
+
413
+ if (isB2BCheckoutSuccess(req.body)) {
414
+ const txId = getB2BTransactionId(req.body);
415
+ const amount = getB2BAmount(req.body);
416
+ console.log("B2B paid:", { txId, amount });
417
+ } else if (isB2BCheckoutCancelled(req.body)) {
418
+ console.log("B2B cancelled by merchant");
419
+ }
420
+ });
421
+ ```
422
+
423
+ **B2B Error codes:**
424
+
425
+ | Code | Meaning |
426
+ | ---- | ------------------------ |
427
+ | 0 | Success |
428
+ | 4001 | User cancelled |
429
+ | 4102 | Merchant KYC fail |
430
+ | 4104 | Missing Nominated Number |
431
+ | 4201 | USSD network error |
432
+ | 4203 | USSD exception error |
433
+
434
+ ---
435
+
436
+ ### Account Balance
437
+
438
+ Query the balance of your M-PESA shortcode. **Asynchronous** β€” result is POSTed to your `resultUrl`.
439
+
440
+ ```typescript
441
+ 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
+ });
448
+ ```
449
+
450
+ **Parsing the result:**
451
+
452
+ ```typescript
453
+ import {
454
+ isAccountBalanceSuccess,
455
+ parseAccountBalance,
456
+ getAccountBalanceParam,
457
+ type AccountBalanceResult,
458
+ } from "pesafy";
459
+
460
+ app.post("/api/mpesa/balance/result", (req, res) => {
461
+ res.json({ ResultCode: 0, ResultDesc: "Accepted" });
462
+
463
+ const body = req.body as AccountBalanceResult;
464
+ if (!isAccountBalanceSuccess(body)) return;
465
+
466
+ const raw = getAccountBalanceParam(body, "AccountBalance") as string;
467
+ const accounts = parseAccountBalance(raw ?? "");
468
+
469
+ for (const account of accounts) {
470
+ console.log(`${account.name}: ${account.currency} ${account.amount}`);
471
+ // e.g. "Working Account: KES 45000.00"
472
+ }
473
+ });
474
+ ```
475
+
476
+ ---
477
+
478
+ ### Transaction Status
479
+
480
+ Query the result of any completed M-PESA transaction. **Asynchronous**.
481
+
482
+ ```typescript
483
+ 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
+ });
491
+ ```
492
+
493
+ ---
494
+
495
+ ### Transaction Reversal
496
+
497
+ Reverse a completed M-PESA transaction. **Asynchronous**.
498
+
499
+ ```typescript
500
+ await mpesa.reverseTransaction({
501
+ transactionId: "OEI2AK4XXXX",
502
+ receiverParty: "174379",
503
+ receiverIdentifierType: "4", // "1"=MSISDN, "2"=Till, "4"=ShortCode
504
+ 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
+ });
509
+ ```
510
+
511
+ **Reversal result handler:**
512
+
513
+ ```typescript
514
+ import { isReversalSuccess, getReversalTransactionId } from "pesafy";
515
+
516
+ app.post("/api/mpesa/reversal/result", (req, res) => {
517
+ res.json({ ResultCode: 0, ResultDesc: "Accepted" });
518
+
519
+ if (isReversalSuccess(req.body)) {
520
+ console.log("Reversed:", getReversalTransactionId(req.body));
521
+ }
522
+ });
523
+ ```
524
+
525
+ ---
526
+
527
+ ### Tax Remittance (KRA)
528
+
529
+ Remit tax to Kenya Revenue Authority via M-PESA. **Asynchronous**.
530
+
531
+ ```typescript
532
+ await mpesa.remitTax({
533
+ 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
+ });
540
+ // PartyB is always KRA_SHORTCODE ("572572") β€” auto-set
541
+ ```
542
+
543
+ ---
544
+
545
+ ### Dynamic QR Code
546
+
547
+ Generate an M-PESA QR code customers can scan to pay.
548
+
549
+ ```typescript
550
+ const qr = await mpesa.generateDynamicQR({
551
+ merchantName: "My Shop",
552
+ refNo: "INV-001",
553
+ amount: 500,
554
+ trxCode: "BG", // "BG"=Buy Goods, "PB"=Paybill, "WA"=Withdraw, "SM"=Send Money
555
+ cpi: "373132", // till / paybill / MSISDN
556
+ size: 300, // pixels (square)
557
+ });
558
+
559
+ // Render in HTML:
560
+ // <img src={`data:image/png;base64,${qr.QRCode}`} />
561
+ ```
562
+
563
+ **QR Transaction codes:**
564
+
565
+ | Code | Use Case |
566
+ | ---- | --------------------------- |
567
+ | `BG` | Pay Merchant (Buy Goods) |
568
+ | `WA` | Withdraw Cash at Agent Till |
569
+ | `PB` | Paybill / Business number |
570
+ | `SM` | Send Money (mobile number) |
571
+ | `SB` | Send to Business |
145
572
 
146
- ## πŸ” Security
573
+ ---
574
+
575
+ ### Bill Manager
576
+
577
+ Create and send invoices customers pay via M-PESA.
578
+
579
+ ```typescript
580
+ // 1. Opt-in your shortcode (once)
581
+ await mpesa.billManagerOptIn({
582
+ shortcode: "600984",
583
+ email: "billing@company.com",
584
+ officialContact: "0700000000",
585
+ sendReminders: "1",
586
+ logo: "https://cdn.company.com/logo.png",
587
+ callbackUrl: "https://yourdomain.com/api/mpesa/bills/callback",
588
+ });
589
+
590
+ // 2. Send a single invoice
591
+ await mpesa.sendInvoice({
592
+ externalReference: "INV-001",
593
+ billingPeriod: "2024-01",
594
+ invoiceName: "January Subscription",
595
+ dueDate: "2024-01-31 23:59:00",
596
+ accountReference: "ACC-12345",
597
+ amount: 2500,
598
+ partyA: "254712345678",
599
+ invoiceItems: [
600
+ { itemName: "Base subscription", amount: 2000 },
601
+ { itemName: "SMS bundle", amount: 500 },
602
+ ],
603
+ });
604
+
605
+ // 3. Bulk invoices (up to 1 000 per call)
606
+ await mpesa.sendBulkInvoices({
607
+ invoices: [
608
+ { externalReference: "INV-002", billingPeriod: "2024-01", ... },
609
+ { externalReference: "INV-003", billingPeriod: "2024-01", ... },
610
+ ],
611
+ });
612
+
613
+ // 4. Cancel an invoice
614
+ await mpesa.cancelInvoice({ externalReference: "INV-001" });
615
+ ```
616
+
617
+ ---
618
+
619
+ ### Webhooks
620
+
621
+ **IP verification** (Safaricom always calls from whitelisted IPs):
622
+
623
+ ```typescript
624
+ import { verifyWebhookIP, SAFARICOM_IPS } from "pesafy";
625
+
626
+ app.post("/api/mpesa/callback", (req, res) => {
627
+ const ip = req.ip ?? req.headers["x-forwarded-for"];
628
+ if (!verifyWebhookIP(ip)) {
629
+ console.warn("Callback from unknown IP:", ip);
630
+ // Still return 200 β€” Safaricom will retry if you reject
631
+ }
632
+ // ... process
633
+ res.json({ ResultCode: 0, ResultDesc: "Accepted" });
634
+ });
635
+ ```
636
+
637
+ **Generic webhook handler:**
638
+
639
+ ```typescript
640
+ import { handleWebhook, isSuccessfulCallback, extractTransactionId, extractAmount } from "pesafy";
641
+
642
+ app.post("/api/mpesa/callback", (req, res) => {
643
+ const result = handleWebhook(req.body, {
644
+ requestIP: req.ip,
645
+ skipIPCheck: false, // set true in local dev
646
+ });
647
+
648
+ if (result.success && isSuccessfulCallback(result.data)) {
649
+ const receipt = extractTransactionId(result.data);
650
+ const amount = extractAmount(result.data);
651
+ }
652
+
653
+ res.json({ ResultCode: 0, ResultDesc: "Accepted" });
654
+ });
655
+ ```
656
+
657
+ **Retry with backoff** (for your own internal processing):
658
+
659
+ ```typescript
660
+ import { retryWithBackoff } from "pesafy";
661
+
662
+ const outcome = await retryWithBackoff(() => saveToDatabase(webhookData), {
663
+ maxRetries: 5,
664
+ initialDelay: 500,
665
+ maxDelay: 30_000,
666
+ });
667
+
668
+ if (!outcome.success) {
669
+ console.error("Failed after", outcome.attempts, "attempts");
670
+ }
671
+ ```
672
+
673
+ ---
674
+
675
+ ## Framework Adapters
676
+
677
+ ### Express Adapter
678
+
679
+ ```typescript
680
+ import express from "express";
681
+ import {
682
+ createMpesaExpressRouter,
683
+ acceptC2BValidation,
684
+ isB2CSuccess,
685
+ getB2CTransactionId,
686
+ } from "pesafy/express";
687
+
688
+ const app = express();
689
+ app.use(express.json());
690
+
691
+ const router = express.Router();
692
+
693
+ createMpesaExpressRouter(router, {
694
+ consumerKey: process.env.MPESA_CONSUMER_KEY!,
695
+ consumerSecret: process.env.MPESA_CONSUMER_SECRET!,
696
+ environment: "sandbox",
697
+ lipaNaMpesaShortCode: "174379",
698
+ lipaNaMpesaPassKey: process.env.MPESA_PASSKEY!,
699
+ callbackUrl: "https://yourdomain.com/api/mpesa/express/callback",
700
+
701
+ // Initiator (for B2C, Reversal, Balance)
702
+ initiatorName: "testapi",
703
+ initiatorPassword: "Safaricom123!",
704
+ certificatePath: "./SandboxCertificate.cer",
705
+
706
+ // Result endpoints
707
+ resultUrl: "https://yourdomain.com/api/mpesa/transaction-status/result",
708
+ queueTimeOutUrl: "https://yourdomain.com/api/mpesa/timeout",
709
+
710
+ // C2B
711
+ c2bShortCode: "600984",
712
+ c2bConfirmationUrl: "https://yourdomain.com/api/mpesa/c2b/confirmation",
713
+ c2bValidationUrl: "https://yourdomain.com/api/mpesa/c2b/validation",
714
+ onC2BValidation: async (payload) => {
715
+ const amount = Number(payload.TransAmount);
716
+ if (amount > 100_000) return { ResultCode: "C2B00013", ResultDesc: "Rejected" };
717
+ return acceptC2BValidation();
718
+ },
719
+ onC2BConfirmation: async (payload) => {
720
+ await db.payments.create({ txId: payload.TransID, amount: Number(payload.TransAmount) });
721
+ },
722
+
723
+ // B2C
724
+ b2cPartyA: "600979",
725
+ b2cResultUrl: "https://yourdomain.com/api/mpesa/b2c/result",
726
+ b2cQueueTimeOutUrl: "https://yourdomain.com/api/mpesa/b2c/timeout",
727
+ onB2CResult: async (result) => {
728
+ if (isB2CSuccess(result)) {
729
+ await db.disbursements.markCompleted({ txId: getB2CTransactionId(result)! });
730
+ }
731
+ },
732
+
733
+ skipIPCheck: true, // local dev only
734
+ });
735
+
736
+ app.use("/api", router);
737
+ app.listen(3000);
738
+ ```
739
+
740
+ **Routes mounted by `createMpesaExpressRouter`:**
741
+
742
+ | Method | Path | Description |
743
+ | ------ | ---------------------------------- | -------------------------------- |
744
+ | POST | `/mpesa/express/stk-push` | Initiate STK Push |
745
+ | POST | `/mpesa/express/stk-query` | Query STK Push status |
746
+ | POST | `/mpesa/express/callback` | STK Push callback from Safaricom |
747
+ | POST | `/mpesa/transaction-status/query` | Query transaction |
748
+ | POST | `/mpesa/transaction-status/result` | Transaction result callback |
749
+ | POST | `/mpesa/c2b/register-url` | Register C2B URLs |
750
+ | POST | `/mpesa/c2b/simulate` | Simulate C2B (sandbox) |
751
+ | POST | `/mpesa/c2b/validation` | C2B validation callback |
752
+ | POST | `/mpesa/c2b/confirmation` | C2B confirmation callback |
753
+ | POST | `/mpesa/tax/remit` | Initiate tax remittance |
754
+ | POST | `/mpesa/tax/result` | Tax remittance result |
755
+ | POST | `/mpesa/b2b/checkout` | B2B Express Checkout |
756
+ | POST | `/mpesa/b2b/callback` | B2B callback |
757
+ | POST | `/mpesa/b2c/payment` | B2C payment |
758
+ | POST | `/mpesa/b2c/result` | B2C result callback |
759
+
760
+ ---
761
+
762
+ ### Hono Adapter
763
+
764
+ Works on Bun, Cloudflare Workers, Deno, and Node.js via Hono.
765
+
766
+ ```typescript
767
+ import { Hono } from "hono";
768
+ import { createMpesaHonoRouter } from "pesafy/adapters/hono";
769
+
770
+ const app = new Hono();
771
+
772
+ createMpesaHonoRouter(app, {
773
+ consumerKey: process.env.MPESA_CONSUMER_KEY!,
774
+ consumerSecret: process.env.MPESA_CONSUMER_SECRET!,
775
+ environment: "sandbox",
776
+ lipaNaMpesaShortCode: "174379",
777
+ 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",
781
+
782
+ onStkSuccess: async ({ receiptNumber, amount, phone }) => {
783
+ await db.payments.create({ receiptNumber, amount, phone });
784
+ },
785
+ onStkFailure: ({ resultCode, resultDesc }) => {
786
+ console.warn("STK failed:", resultCode, resultDesc);
787
+ },
788
+ skipIPCheck: true,
789
+ });
790
+
791
+ // Bun
792
+ export default app;
793
+
794
+ // Cloudflare Workers
795
+ export default { fetch: app.fetch };
796
+ ```
797
+
798
+ ---
799
+
800
+ ### Next.js Adapter
801
+
802
+ ```typescript
803
+ // app/api/mpesa/stk-push/route.ts
804
+ import { createStkPushHandler } from "pesafy/adapters/nextjs";
805
+
806
+ export const POST = createStkPushHandler({
807
+ consumerKey: process.env.MPESA_CONSUMER_KEY!,
808
+ consumerSecret: process.env.MPESA_CONSUMER_SECRET!,
809
+ environment: "sandbox",
810
+ lipaNaMpesaShortCode: process.env.MPESA_SHORTCODE!,
811
+ lipaNaMpesaPassKey: process.env.MPESA_PASSKEY!,
812
+ callbackUrl: process.env.MPESA_CALLBACK_URL!,
813
+ });
814
+ ```
815
+
816
+ ```typescript
817
+ // app/api/mpesa/callback/route.ts
818
+ import { createStkCallbackHandler } from "pesafy/adapters/nextjs";
819
+
820
+ export const POST = createStkCallbackHandler({
821
+ consumerKey: process.env.MPESA_CONSUMER_KEY!,
822
+ consumerSecret: process.env.MPESA_CONSUMER_SECRET!,
823
+ environment: "sandbox",
824
+ callbackUrl: process.env.MPESA_CALLBACK_URL!,
825
+ onSuccess: async ({ receiptNumber, amount, phone }) => {
826
+ await db.payments.create({ receiptNumber, amount: amount ?? 0, phone: phone ?? "" });
827
+ },
828
+ onFailure: ({ resultCode, resultDesc }) => {
829
+ console.warn("Payment failed:", resultCode, resultDesc);
830
+ },
831
+ });
832
+ ```
833
+
834
+ ```typescript
835
+ // app/api/mpesa/stk-query/route.ts
836
+ import { createStkQueryHandler } from "pesafy/adapters/nextjs";
837
+
838
+ export const POST = createStkQueryHandler({
839
+ consumerKey: process.env.MPESA_CONSUMER_KEY!,
840
+ consumerSecret: process.env.MPESA_CONSUMER_SECRET!,
841
+ environment: "sandbox",
842
+ lipaNaMpesaShortCode: process.env.MPESA_SHORTCODE!,
843
+ lipaNaMpesaPassKey: process.env.MPESA_PASSKEY!,
844
+ callbackUrl: process.env.MPESA_CALLBACK_URL!,
845
+ });
846
+ ```
847
+
848
+ ---
849
+
850
+ ### Fastify Adapter
851
+
852
+ ```typescript
853
+ import Fastify from "fastify";
854
+ import { registerMpesaRoutes } from "pesafy/adapters/fastify";
855
+
856
+ const app = Fastify({ logger: true });
857
+
858
+ await registerMpesaRoutes(app, {
859
+ consumerKey: process.env.MPESA_CONSUMER_KEY!,
860
+ consumerSecret: process.env.MPESA_CONSUMER_SECRET!,
861
+ environment: "sandbox",
862
+ lipaNaMpesaShortCode: "174379",
863
+ 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",
867
+ skipIPCheck: true,
868
+ onStkSuccess: async ({ receiptNumber, amount, phone }) => {
869
+ app.log.info({ receiptNumber, amount, phone }, "Payment received");
870
+ },
871
+ });
872
+
873
+ await app.listen({ port: 3000 });
874
+ ```
875
+
876
+ ---
877
+
878
+ ## Branded Types
879
+
880
+ pesafy ships opt-in branded primitives that catch type bugs at compile time β€” not at runtime.
881
+
882
+ ```typescript
883
+ import {
884
+ toKesAmount,
885
+ toMsisdn,
886
+ toPaybill,
887
+ type KesAmount,
888
+ type MsisdnKE,
889
+ type PaybillCode,
890
+ } from "pesafy";
891
+
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");
895
+
896
+ // βœ… Safe β€” editor shows exact types
897
+ // ❌ Compile error β€” can't pass plain number where KesAmount is expected
898
+ ```
899
+
900
+ **Result type** β€” prefer this over try/catch in application code:
901
+
902
+ ```typescript
903
+ import { ok, err, type Result } from "pesafy";
147
904
 
148
- For security concerns, please email [lewisodero27@gmail.com](mailto:lewisodero27@gmail.com) instead of using the issue tracker.
905
+ const result: Result<string> = await mpesa.stkPushSafe({ ... });
149
906
 
150
- ## πŸ“„ License
907
+ if (result.ok) {
908
+ console.log(result.data.CheckoutRequestID);
909
+ } else {
910
+ // result.error is PesafyError with .code, .statusCode, .retryable
911
+ if (result.error.retryable) {
912
+ // schedule retry
913
+ }
914
+ }
915
+ ```
916
+
917
+ ---
918
+
919
+ ## Error Handling
920
+
921
+ All errors are `PesafyError` instances:
922
+
923
+ ```typescript
924
+ import { PesafyError, isPesafyError } from "pesafy";
925
+
926
+ try {
927
+ await mpesa.stkPush({ ... });
928
+ } catch (error) {
929
+ if (isPesafyError(error)) {
930
+ console.log(error.code); // "AUTH_FAILED" | "VALIDATION_ERROR" | "API_ERROR" | ...
931
+ console.log(error.message);
932
+ console.log(error.statusCode); // HTTP status (if applicable)
933
+ console.log(error.retryable); // boolean β€” safe to retry?
934
+ console.log(error.requestId); // Daraja requestId (if returned)
935
+
936
+ // Convenience properties
937
+ error.isValidation; // true if VALIDATION_ERROR
938
+ error.isAuth; // true if AUTH_FAILED / INVALID_CREDENTIALS
939
+ }
940
+ }
941
+ ```
151
942
 
152
- This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
943
+ **Error codes:**
153
944
 
154
- ## πŸ™ Acknowledgments
945
+ | Code | Meaning |
946
+ | --------------------- | ------------------------------------------- |
947
+ | `AUTH_FAILED` | OAuth token fetch failed |
948
+ | `INVALID_CREDENTIALS` | Missing or wrong consumerKey/Secret |
949
+ | `INVALID_PHONE` | Phone number cannot be normalised |
950
+ | `ENCRYPTION_FAILED` | RSA encryption of initiator password failed |
951
+ | `VALIDATION_ERROR` | Invalid request parameters (do not retry) |
952
+ | `API_ERROR` | Daraja returned a 4xx error |
953
+ | `REQUEST_FAILED` | Daraja returned 5xx (retryable) |
954
+ | `NETWORK_ERROR` | DNS / connection failure (retryable) |
955
+ | `TIMEOUT` | Request exceeded timeout (retryable) |
956
+ | `RATE_LIMITED` | 429 Too Many Requests |
155
957
 
156
- - [Safaricom Daraja API](https://developer.safaricom.co.ke/) - For providing the M-Pesa API
157
- - Inspired by payment gateway patterns from Stripe and other industry leaders
958
+ ---
959
+
960
+ ## Utilities
158
961
 
159
- ## πŸ“ž Support
962
+ ### Phone number formatting
160
963
 
161
- - πŸ“§ Email: [lewisodero27@gmail.com](mailto:lewisodero27@gmail.com)
162
- - πŸ› Issues: [GitHub Issues](https://github.com/levos-snr/pesafy/issues)
163
- - πŸ“– Documentation: [Full Documentation](./docs/)
964
+ ```typescript
965
+ import { formatSafaricomPhone } from "pesafy";
966
+
967
+ formatSafaricomPhone("0712345678"); // β†’ "254712345678"
968
+ formatSafaricomPhone("+254712345678"); // β†’ "254712345678"
969
+ formatSafaricomPhone("712345678"); // β†’ "254712345678"
970
+ formatSafaricomPhone("254712345678"); // β†’ "254712345678"
971
+ ```
164
972
 
165
- ## πŸ—ΊοΈ Roadmap
973
+ ### Security credential encryption
166
974
 
167
- See [PROJECT_PLAN.md](./PROJECT_PLAN.md) for the complete development roadmap.
975
+ ```typescript
976
+ import { encryptSecurityCredential } from "pesafy";
977
+ import { readFileSync } from "fs";
168
978
 
169
- **Phase 1** (Current): Core library foundation βœ…
170
- **Phase 2**: Payment processing implementation 🚧
171
- **Phase 3**: Webhook management system πŸ“…
172
- **Phase 4**: Dashboard development πŸ“…
173
- **Phase 5**: Payment components πŸ“…
979
+ const pem = readFileSync("./SandboxCertificate.cer", "utf-8");
980
+ const credential = encryptSecurityCredential("Safaricom123!", pem);
981
+ // Pass as config.securityCredential to skip per-call encryption
982
+ ```
174
983
 
175
984
  ---
176
985
 
177
- Made with ❀️ for the African developer community
986
+ ## Configuration Reference
987
+
988
+ | Option | Type | Required | Default | Description |
989
+ | ---------------------- | --------------------------- | ------------------------ | ------- | ------------------------------------ |
990
+ | `consumerKey` | `string` | βœ… | β€” | Daraja consumer key |
991
+ | `consumerSecret` | `string` | βœ… | β€” | Daraja consumer secret |
992
+ | `environment` | `"sandbox" \| "production"` | βœ… | β€” | Target environment |
993
+ | `lipaNaMpesaShortCode` | `string` | STK Push | β€” | Paybill / HO shortcode |
994
+ | `lipaNaMpesaPassKey` | `string` | STK Push | β€” | LNM passkey |
995
+ | `initiatorName` | `string` | B2C / Reversal / Balance | β€” | API operator username |
996
+ | `initiatorPassword` | `string` | B2C / Reversal / Balance | β€” | API operator password |
997
+ | `certificatePath` | `string` | B2C / Reversal / Balance | β€” | Path to `.cer` file |
998
+ | `certificatePem` | `string` | β€” | β€” | PEM string (alternative to path) |
999
+ | `securityCredential` | `string` | β€” | β€” | Pre-encrypted credential (skips RSA) |
1000
+ | `retries` | `number` | β€” | `4` | Retry count on transient errors |
1001
+ | `retryDelay` | `number` | β€” | `2000` | Base retry delay (ms) |
1002
+ | `timeout` | `number` | β€” | `30000` | Per-attempt timeout (ms) |
1003
+
1004
+ ---
1005
+
1006
+ ## Roadmap
1007
+
1008
+ ### Planned (Safaricom APIs)
1009
+
1010
+ - [ ] **Standing Orders** β€” create recurring M-PESA payment instructions
1011
+ - [ ] **M-PESA Global (Send Money)** β€” international transfers
1012
+ - [ ] **Ratiba (M-PESA Ratiba)** β€” recurring bill payments
1013
+ - [ ] **M-PESA for Business** β€” bulk payments improvements
1014
+ - [ ] **Merchant QR** β€” static QR code generation
1015
+
1016
+ ### Planned (Library)
1017
+
1018
+ - [ ] **Idempotency keys** β€” automatic deduplication headers
1019
+ - [ ] **Webhook signature verification** β€” once Safaricom ships HMAC support
1020
+ - [ ] **React hooks** β€” `useStkPush()`, `usePaymentStatus()` with polling
1021
+ - [ ] **Vue composables** β€” `useStkPush()` for Vue 3
1022
+ - [ ] **OpenAPI spec** β€” auto-generated from types
1023
+ - [ ] **Mock server** β€” offline Daraja sandbox for unit testing
1024
+ - [ ] **Zod schemas** β€” runtime validation of all request / response shapes
1025
+ - [ ] **SvelteKit adapter** β€” `createMpesaSvelteHandler()`
1026
+ - [ ] **Astro adapter** β€” API route helpers
1027
+
1028
+ ---
1029
+
1030
+ ## Contributing
1031
+
1032
+ 1. Fork the repository
1033
+ 2. Create your feature branch: `git checkout -b feat/my-feature`
1034
+ 3. Commit: `git commit -m '✨ Add my feature'`
1035
+ 4. Push: `git push origin feat/my-feature`
1036
+ 5. Open a Pull Request
1037
+
1038
+ ---
1039
+
1040
+ ## License
1041
+
1042
+ MIT Β© [Lewis Odero](https://github.com/levos-snr)
1043
+
1044
+ ---
1045
+
1046
+ ## Support
1047
+
1048
+ - πŸ› [GitHub Issues](https://github.com/levos-snr/pesafy/issues)
1049
+ - πŸ“§ [lewisodero27@gmail.com](mailto:lewisodero27@gmail.com)
1050
+ - πŸ“– [Daraja Docs](https://developer.safaricom.co.ke)