mpesa_stk 1.3 → 2.0.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.
data/README.md CHANGED
@@ -1,5 +1,30 @@
1
1
  # MpesaStk
2
- Lipa na M-Pesa Online Payment API is used to initiate a M-Pesa transaction on behalf of a customer using STK Push. This is the same technique mySafaricom App uses whenever the app is used to make payments.
2
+
3
+ Copyright (c) 2018 mboya
4
+
5
+ MIT License
6
+
7
+ Permission is hereby granted, free of charge, to any person obtaining a copy
8
+ of this software and associated documentation files (the "Software"), to deal
9
+ in the Software without restriction, including without limitation the rights
10
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11
+ copies of the Software, and to permit persons to whom the Software is
12
+ furnished to do so, subject to the following conditions:
13
+
14
+ The above copyright notice and this permission notice shall be included in
15
+ all copies or substantial portions of the Software.
16
+
17
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
23
+ THE SOFTWARE.
24
+
25
+ ---
26
+
27
+ A comprehensive Ruby gem for integrating with Safaricom's M-Pesa APIs, IoT services, and related payment solutions. This gem provides easy-to-use interfaces for STK Push, B2C, B2B, C2B, transaction queries, standing orders, IoT SIM management, and more.
3
28
 
4
29
  [![Gem Version](https://badge.fury.io/rb/mpesa_stk.svg)](https://badge.fury.io/rb/mpesa_stk.svg)
5
30
  ![Cop](https://github.com/mboya/mpesa_stk/workflows/Cop/badge.svg?branch=master)
@@ -19,37 +44,53 @@ gem install mpesa_stk
19
44
  ```
20
45
 
21
46
  # Getting Started
22
- This gem has a [Redis](https://redis.io/) dependency, so make sure it running
23
- ```ruby
47
+
48
+ ## Prerequisites
49
+
50
+ **Redis:** This gem has a [Redis](https://redis.io/) dependency, so make sure it's running:
51
+ ```bash
24
52
  $ redis-server
25
53
  ```
26
- You can use command line to determine if redis is running:
27
- ```ruby
28
- redis-cli ping
54
+
55
+ You can verify Redis is running:
56
+ ```bash
57
+ $ redis-cli ping
58
+ # Should return: PONG
29
59
  ```
30
- you should get back
31
- ```ruby
32
- PONG
60
+
61
+ **Bundler:** Ensure you have a recent version of bundler (2.4+ recommended for Ruby 3.3+):
62
+ ```bash
63
+ $ gem install bundler
64
+ $ bundle --version # Should show 2.4.0 or higher
33
65
  ```
34
66
 
67
+ > **Note:** If you see deprecation warnings about `Gem::Platform.match` or `DidYouMean::SPELL_CHECKERS`, update bundler with `gem install bundler`. These warnings come from older bundler versions and don't affect gem functionality.
68
+
35
69
  you need to setup your environment variables, checkout `.sample.env` for the values you need.
36
70
  or run
37
71
  ```ruby
38
72
  $ cp .sample.env .env
39
73
  ```
40
- open `.env` on your editor and add the missing variable
41
- ```
42
- key=""
43
- secret=""
44
- business_short_code=""
45
- business_passkey=""
46
- callback_url=""
47
- till_number=""
48
- ```
74
+ open `.env` on your editor and add the missing variables. See `.sample.env` for all available configuration options.
49
75
 
50
- * `key` and `secret` of the app created on your [developer account](https://developer.safaricom.co.ke/user/me/apps).
51
- * `business_short_code` and `business_pass_key` this can be found in [Test Credentials](https://developer.safaricom.co.ke/test_credentials).
52
- * `callback_url` the url of your application where response will be sent. `make sure its a reachable/active url`
76
+ **Required for STK Push:**
77
+ * `key` and `secret` - Consumer key and secret from your [developer account](https://developer.safaricom.co.ke/user/me/apps)
78
+ * `business_short_code` and `business_passkey` - Found in [Test Credentials](https://developer.safaricom.co.ke/test_credentials)
79
+ * `callback_url` - Your webhook URL for receiving payment confirmations (must be HTTPS and reachable)
80
+
81
+ **Required for B2C/B2B/Reversal/Transaction Status:**
82
+ * `initiator` or `initiator_name` - API user name
83
+ * `security_credential` - Encrypted initiator password (use Safaricom's public key)
84
+ * `result_url` - Webhook URL for async results
85
+ * `queue_timeout_url` - Webhook URL for timeout notifications
86
+
87
+ **For C2B:**
88
+ * `confirmation_url` - URL for payment confirmations
89
+
90
+ **For IoT APIs:**
91
+ * `iot_api_key` - API key for IoT services (default provided for sandbox)
92
+ * `vpn_group` - VPN group identifier
93
+ * `username` - Username for IoT operations
53
94
 
54
95
  `Prod:`
55
96
 
@@ -69,75 +110,957 @@ https://github.com/mboya/stk
69
110
  #### Sample application
70
111
  Check out a rails sample application [here](https://github.com/mboya/stk)
71
112
 
72
- ### Testing the gem on the console/app
73
- When running the gem on a single safaricom app.
113
+ ### Quick Start Examples
74
114
 
115
+ **Single App (using ENV variables):**
75
116
  ```ruby
76
117
  $ irb
77
- 2.5.0 :001 > require 'mpesa_stk'
78
- 2.5.0 :002 > MpesaStk::PushPayment.call("500", "<YOUR PHONE NUMBER: 254711222333>")
118
+ > require 'mpesa_stk'
119
+ > MpesaStk::PushPayment.call("500", "254711222333")
79
120
  ```
80
121
 
81
- When running the app on multiple safaricom apps, within the same project.
122
+ **Multiple Apps (using hash parameters):**
82
123
  ```ruby
83
124
  $ irb
84
- 2.5.3 :001 > require 'mpesa_stk'
85
- 2.5.3 :002 > hash = Hash.new
86
- 2.5.3 :003 >
87
- 2.5.3 :004 > hash['key'] = key
88
- 2.5.3 :005 > hash['secret'] = secret
89
- 2.5.3 :006 > hash['business_short_code'] = business_short_code
90
- 2.5.3 :007 > hash['business_passkey'] = business_passkey
91
- 2.5.3 :008 > hash['callback_url'] = callback_url
92
- 2.5.3 :009 > hash['till_number'] = till_number
125
+ > require 'mpesa_stk'
126
+ > hash = {
127
+ "key" => "your_key",
128
+ "secret" => "your_secret",
129
+ "business_short_code" => "174379",
130
+ "business_passkey" => "your_passkey",
131
+ "callback_url" => "https://your-app.com/callback",
132
+ "till_number" => "174379"
133
+ }
134
+ > MpesaStk::Push.pay_bill("500", "254711222333", hash) # Pay Bill
135
+ > MpesaStk::Push.buy_goods("500", "254711222333", hash) # Till Number
136
+ ```
137
+
138
+ > **Note:** For complete API documentation with all available endpoints, response formats, and detailed examples, see the [API Reference](#api-reference) and [Response Format & Error Handling](#response-format--error-handling) sections below.
139
+
140
+ ### Mpesa Checkout/Express
141
+
142
+ After initiating an STK Push, the customer will see a payment prompt on their phone. This is the expected output:
143
+
144
+ ![alt tag](./bin/index.jpeg)
145
+
146
+ ### Callback URL Requirements
147
+
148
+ Before implementing callbacks, note these requirements:
149
+ - **HTTPS Required**: All callback URLs must use HTTPS (not HTTP)
150
+ - **Publicly Accessible**: Your callback URLs must be reachable from the internet
151
+ - **Response Expected**: Your endpoint should return HTTP 200 OK to acknowledge receipt
152
+ - **Timeout**: Safaricom expects a response within 30 seconds
153
+
154
+ ## API Reference
155
+
156
+ ### STK Push (Lipa na M-Pesa Online)
157
+
158
+ Initiates a payment prompt on the customer's phone. The customer receives an STK push notification and can complete the payment by entering their M-Pesa PIN. This is the same technique the mySafaricom App uses for payments.
159
+
160
+ **Single App (using ENV variables):**
161
+ ```ruby
162
+ result = MpesaStk::PushPayment.call("500", "254712345678")
163
+ ```
164
+
165
+ **Multiple Apps (using hash parameters):**
166
+ ```ruby
167
+ hash = {
168
+ "key" => "your_key",
169
+ "secret" => "your_secret",
170
+ "business_short_code" => "174379",
171
+ "business_passkey" => "your_passkey",
172
+ "callback_url" => "https://your-app.com/callback"
173
+ }
174
+
175
+ # Pay Bill
176
+ result = MpesaStk::Push.pay_bill("500", "254712345678", hash)
177
+
178
+ # Buy Goods (Till Number)
179
+ hash["till_number"] = "174379"
180
+ result = MpesaStk::Push.buy_goods("500", "254712345678", hash)
181
+ ```
182
+
183
+ **Query STK Push Status:**
184
+
185
+ Check the status of an STK Push transaction using the CheckoutRequestID returned from the initial STK Push request.
186
+
187
+ ```ruby
188
+ # Using ENV variables
189
+ result = MpesaStk::StkPushQuery.query("ws_CO_DMZ_40472724_16062018092359957")
190
+
191
+ # Using hash parameters
192
+ result = MpesaStk::StkPushQuery.query("ws_CO_DMZ_40472724_16062018092359957", {
193
+ "business_short_code" => "174379",
194
+ "business_passkey" => "your_passkey"
195
+ })
196
+ ```
197
+
198
+ **STK Push Callbacks:**
199
+
200
+ After the customer enters their PIN on the checkout/express prompt, you will receive a POST request on your `callback_url` with the transaction status.
201
+
202
+ **Sample Callback Payload:**
203
+ ```ruby
204
+ {
205
+ "Body" => {
206
+ "stkCallback" => {
207
+ "MerchantRequestID" => "3968-94214-1",
208
+ "CheckoutRequestID" => "ws_CO_160620191218268004",
209
+ "ResultCode" => 0,
210
+ "ResultDesc" => "The service request is processed successfully.",
211
+ "CallbackMetadata" => {
212
+ "Item" => [
213
+ {"Name" => "Amount", "Value" => "05"},
214
+ {"Name" => "MpesaReceiptNumber", "Value" => "OFG4Z5EE9Y"},
215
+ {"Name" => "TransactionDate", "Value" => 20190616121848},
216
+ {"Name" => "PhoneNumber", "Value" => 254711222333}
217
+ ]
218
+ }
219
+ }
220
+ }
221
+ }
222
+ ```
223
+
224
+ **Result Codes:**
225
+ - `0` - Success
226
+ - `1032` - Request cancelled by user
227
+ - `1037` - Timeout waiting for user input
228
+ - `2001` - Insufficient balance
229
+ - Other codes indicate various error conditions
230
+
231
+ ### Transaction Status Query
232
+
233
+ Query the status of any M-Pesa transaction using the transaction ID (M-Pesa Receipt Number). Useful for checking if a payment was successful, failed, or is still pending.
234
+
235
+ ```ruby
236
+ # Using ENV variables
237
+ result = MpesaStk::TransactionStatus.query("OFR4Z5EE9Y")
238
+
239
+ # Using hash parameters
240
+ result = MpesaStk::TransactionStatus.query("OFR4Z5EE9Y", {
241
+ "initiator" => "testapi",
242
+ "security_credential" => "encrypted_credential",
243
+ "result_url" => "https://your-app.com/result",
244
+ "queue_timeout_url" => "https://your-app.com/timeout"
245
+ })
246
+ ```
247
+
248
+ **Transaction Status Callbacks:**
249
+
250
+ Transaction status queries send results to `result_url` and `queue_timeout_url`.
251
+
252
+ **Sample Result Callback Payload:**
253
+ ```ruby
254
+ {
255
+ "Result" => {
256
+ "ResultType" => 0,
257
+ "ResultCode" => 0,
258
+ "ResultDesc" => "The service request is processed successfully.",
259
+ "OriginatorConversationID" => "12345-67890-1",
260
+ "ConversationID" => "AG_20200101_00001234567890",
261
+ "TransactionID" => "LGR123456789",
262
+ "ResultParameters" => {
263
+ "ResultParameter" => [
264
+ {"Key" => "TransactionStatus", "Value" => "Completed"},
265
+ {"Key" => "TransactionAmount", "Value" => "100.00"},
266
+ {"Key" => "TransactionReceipt", "Value" => "LGR123456789"},
267
+ {"Key" => "B2CWorkingAccountAvailableFunds", "Value" => "50000.00"},
268
+ {"Key" => "B2CUtilityAccountAvailableFunds", "Value" => "100000.00"},
269
+ {"Key" => "TransactionCompletedDateTime", "Value" => "01.01.2020 12:00:00"},
270
+ {"Key" => "ReceiverPartyPublicName", "Value" => "254712345678 - John Doe"}
271
+ ]
272
+ }
273
+ }
274
+ }
275
+ ```
276
+
277
+ ### B2C (Business to Customer)
278
+
279
+ Send money from a business account to a customer's mobile money account. Commonly used for salary payments, refunds, cashback, or any business-to-customer disbursements. The customer receives the money directly in their M-Pesa account.
280
+
281
+ **Using ENV variables:**
282
+ ```ruby
283
+ # Set ENV variables: initiator_name, security_credential, result_url, queue_timeout_url
284
+ result = MpesaStk::B2C.pay("500", "254712345678", {
285
+ "command_id" => "BusinessPayment", # or "SalaryPayment", "PromotionPayment"
286
+ "remarks" => "Payment for services",
287
+ "occasion" => "Optional occasion" # optional
288
+ })
289
+ ```
290
+
291
+ **Using hash parameters:**
292
+ ```ruby
293
+ result = MpesaStk::B2C.pay("500", "254712345678", {
294
+ "key" => "your_key",
295
+ "secret" => "your_secret",
296
+ "initiator_name" => "testapi",
297
+ "security_credential" => "encrypted_credential",
298
+ "command_id" => "BusinessPayment", # or "SalaryPayment", "PromotionPayment"
299
+ "remarks" => "Payment for services",
300
+ "occasion" => "Optional occasion", # optional
301
+ "result_url" => "https://your-app.com/result",
302
+ "queue_timeout_url" => "https://your-app.com/timeout"
303
+ })
304
+ ```
305
+
306
+ **B2C Callbacks:**
307
+
308
+ B2C payments send results to two callback URLs:
309
+ - **Result URL** (`result_url`): Receives transaction results (success or failure)
310
+ - **Queue Timeout URL** (`queue_timeout_url`): Receives timeout notifications
311
+
312
+ **Sample Result Callback Payload:**
313
+ ```ruby
314
+ {
315
+ "Result" => {
316
+ "ResultType" => 0,
317
+ "ResultCode" => 0,
318
+ "ResultDesc" => "The service request is processed successfully.",
319
+ "OriginatorConversationID" => "12345-67890-1",
320
+ "ConversationID" => "AG_20200101_00001234567890",
321
+ "TransactionID" => "LGR123456789",
322
+ "ResultParameters" => {
323
+ "ResultParameter" => [
324
+ {"Key" => "TransactionAmount", "Value" => "100.00"},
325
+ {"Key" => "TransactionReceipt", "Value" => "LGR123456789"},
326
+ {"Key" => "B2CRecipientIsRegisteredCustomer", "Value" => "Y"},
327
+ {"Key" => "B2CChargesPaidAccountAvailableFunds", "Value" => "-100.00"},
328
+ {"Key" => "ReceiverPartyPublicName", "Value" => "254712345678 - John Doe"},
329
+ {"Key" => "TransactionCompletedDateTime", "Value" => "01.01.2020 12:00:00"},
330
+ {"Key" => "B2CUtilityAccountAvailableFunds", "Value" => "50000.00"},
331
+ {"Key" => "B2CWorkingAccountAvailableFunds", "Value" => "100000.00"}
332
+ ]
333
+ },
334
+ "ReferenceData" => {
335
+ "ReferenceItem" => [
336
+ {"Key" => "QueueTimeoutURL", "Value" => "https://your-app.com/timeout"}
337
+ ]
338
+ }
339
+ }
340
+ }
341
+ ```
342
+
343
+ **Result Codes:**
344
+ - `0` - Success
345
+ - `1` - Insufficient balance
346
+ - `2` - Less than minimum transaction value
347
+ - `4` - More than maximum transaction value
348
+ - `5` - Would exceed daily transfer limit
349
+ - `6` - Would exceed minimum balance
350
+ - `8` - Customer account not found
351
+ - `11` - Timeout
352
+
353
+ ### B2B (Business to Business)
354
+
355
+ Send money from one business account to another business account. Used for business-to-business payments such as supplier payments, service payments, or inter-business transfers. Supports both PayBill and Till Number recipients.
356
+
357
+ **Using ENV variables:**
358
+ ```ruby
359
+ # Set ENV variables: initiator, security_credential, result_url, queue_timeout_url
360
+ # Pay Bill
361
+ result = MpesaStk::B2B.pay("1000", "123456", {
362
+ "command_id" => "BusinessPayBill", # or "BusinessBuyGoods", "DisburseFundsToBusiness"
363
+ "receiver_identifier_type" => "4" # "4" for paybill, "2" for till
364
+ })
365
+
366
+ # Buy Goods
367
+ result = MpesaStk::B2B.pay("1000", "987654", {
368
+ "command_id" => "BusinessBuyGoods",
369
+ "receiver_identifier_type" => "2"
370
+ })
371
+ ```
372
+
373
+ **Using hash parameters:**
374
+ ```ruby
375
+ # Pay Bill
376
+ result = MpesaStk::B2B.pay("1000", "123456", {
377
+ "key" => "your_key",
378
+ "secret" => "your_secret",
379
+ "initiator" => "testapi",
380
+ "security_credential" => "encrypted_credential",
381
+ "command_id" => "BusinessPayBill",
382
+ "receiver_identifier_type" => "4",
383
+ "result_url" => "https://your-app.com/result",
384
+ "queue_timeout_url" => "https://your-app.com/timeout"
385
+ })
386
+
387
+ # Buy Goods
388
+ result = MpesaStk::B2B.pay("1000", "987654", {
389
+ "key" => "your_key",
390
+ "secret" => "your_secret",
391
+ "initiator" => "testapi",
392
+ "security_credential" => "encrypted_credential",
393
+ "command_id" => "BusinessBuyGoods",
394
+ "receiver_identifier_type" => "2",
395
+ "result_url" => "https://your-app.com/result",
396
+ "queue_timeout_url" => "https://your-app.com/timeout"
397
+ })
398
+ ```
399
+
400
+ **B2B Callbacks:**
401
+
402
+ Similar to B2C, B2B payments use `result_url` and `queue_timeout_url` for callbacks.
403
+
404
+ **Sample Result Callback Payload:**
405
+ ```ruby
406
+ {
407
+ "Result" => {
408
+ "ResultType" => 0,
409
+ "ResultCode" => 0,
410
+ "ResultDesc" => "The service request is processed successfully.",
411
+ "OriginatorConversationID" => "12345-67890-1",
412
+ "ConversationID" => "AG_20200101_00001234567890",
413
+ "TransactionID" => "LGR123456789",
414
+ "ResultParameters" => {
415
+ "ResultParameter" => [
416
+ {"Key" => "TransactionAmount", "Value" => "1000.00"},
417
+ {"Key" => "TransactionReceipt", "Value" => "LGR123456789"},
418
+ {"Key" => "B2BWorkingAccountAvailableFunds", "Value" => "50000.00"},
419
+ {"Key" => "ReceiverPartyPublicName", "Value" => "600000 - Company Name"},
420
+ {"Key" => "TransactionCompletedDateTime", "Value" => "01.01.2020 12:00:00"},
421
+ {"Key" => "B2BUtilityAccountAvailableFunds", "Value" => "100000.00"}
422
+ ]
423
+ }
424
+ }
425
+ }
426
+ ```
427
+
428
+ ### C2B (Customer to Business)
429
+
430
+ Enable customers to make payments directly to your business account. Customers initiate payments from their phones, and your business receives notifications.
431
+
432
+ **Register URLs:**
433
+
434
+ Register confirmation and validation URLs that Safaricom will call when customers make payments to your PayBill or Till Number. This is a one-time setup required before receiving C2B payments.
435
+
436
+ **Using ENV variables:**
437
+ ```ruby
438
+ # Set ENV variables: confirmation_url
439
+ result = MpesaStk::C2B.register_url({
440
+ "validation_url" => "https://your-app.com/validation", # optional
441
+ "response_type" => "Completed" # or "Cancelled"
442
+ })
443
+ ```
444
+
445
+ **Using hash parameters:**
446
+ ```ruby
447
+ result = MpesaStk::C2B.register_url({
448
+ "key" => "your_key",
449
+ "secret" => "your_secret",
450
+ "short_code" => "174379",
451
+ "confirmation_url" => "https://your-app.com/confirmation",
452
+ "validation_url" => "https://your-app.com/validation", # optional
453
+ "response_type" => "Completed" # or "Cancelled"
454
+ })
455
+ ```
456
+
457
+ **Simulate Payment (Sandbox only):**
458
+
459
+ Simulate a customer payment in the sandbox environment. This allows you to test your C2B integration without requiring actual customer payments. Only available in sandbox, not production.
460
+
461
+ **Using ENV variables:**
462
+ ```ruby
463
+ # Set ENV variables: business_short_code
464
+ result = MpesaStk::C2B.simulate("100", "254712345678", {
465
+ "command_id" => "CustomerPayBillOnline", # or "CustomerBuyGoodsOnline"
466
+ "bill_ref_number" => "INV001" # optional
467
+ })
468
+ ```
469
+
470
+ **Using hash parameters:**
471
+ ```ruby
472
+ result = MpesaStk::C2B.simulate("100", "254712345678", {
473
+ "key" => "your_key",
474
+ "secret" => "your_secret",
475
+ "short_code" => "174379",
476
+ "command_id" => "CustomerPayBillOnline", # or "CustomerBuyGoodsOnline"
477
+ "bill_ref_number" => "INV001" # optional
478
+ })
479
+ ```
480
+
481
+ **C2B Callbacks:**
482
+
483
+ C2B payments use two types of callbacks:
484
+ - **Validation URL**: Called when a payment is initiated (you can accept or reject)
485
+ - **Confirmation URL** (`confirmation_url`): Called when payment is completed
486
+
487
+ **Sample Validation Callback Payload:**
488
+ ```ruby
489
+ {
490
+ "TransactionType" => "Pay Bill",
491
+ "TransID" => "LGR123456789",
492
+ "TransTime" => "20200101120000",
493
+ "TransAmount" => "100.00",
494
+ "BusinessShortCode" => "600000",
495
+ "BillRefNumber" => "Invoice123",
496
+ "InvoiceNumber" => "",
497
+ "OrgAccountBalance" => "50000.00",
498
+ "ThirdPartyTransID" => "",
499
+ "MSISDN" => "254712345678",
500
+ "FirstName" => "John",
501
+ "MiddleName" => "",
502
+ "LastName" => "Doe"
503
+ }
504
+ ```
505
+
506
+ **Sample Confirmation Callback Payload:**
507
+ ```ruby
508
+ {
509
+ "TransactionType" => "Pay Bill",
510
+ "TransID" => "LGR123456789",
511
+ "TransTime" => "20200101120000",
512
+ "TransAmount" => "100.00",
513
+ "BusinessShortCode" => "600000",
514
+ "BillRefNumber" => "Invoice123",
515
+ "InvoiceNumber" => "",
516
+ "OrgAccountBalance" => "50000.00",
517
+ "ThirdPartyTransID" => "",
518
+ "MSISDN" => "254712345678",
519
+ "FirstName" => "John",
520
+ "MiddleName" => "",
521
+ "LastName" => "Doe"
522
+ }
523
+ ```
524
+
525
+ **Validation Response**: Your validation endpoint should return:
526
+ ```ruby
527
+ {
528
+ "ResultCode" => 0, # 0 = Accept, C2B00011 = Reject
529
+ "ResultDesc" => "Accepted"
530
+ }
531
+ ```
532
+
533
+ ### Account Balance
534
+
535
+ Query the current balance of your business M-Pesa account (PayBill or Till Number). Returns the available balance and other account details. Results are sent asynchronously to your ResultURL.
536
+
537
+ **Using ENV variables:**
538
+ ```ruby
539
+ # Set ENV variables: initiator, security_credential, result_url, queue_timeout_url
540
+ result = MpesaStk::AccountBalance.query({})
93
541
  ```
94
- for STK push
542
+
543
+ **Using hash parameters:**
95
544
  ```ruby
96
- 2.5.3 :010 > MpesaStk::Push.pay_bill('05', "<YOUR PHONE NUMBER: 254711222333>", hash)
545
+ result = MpesaStk::AccountBalance.query({
546
+ "key" => "your_key",
547
+ "secret" => "your_secret",
548
+ "initiator" => "testapi",
549
+ "security_credential" => "encrypted_credential",
550
+ "party_a" => "174379", # optional, defaults to business_short_code
551
+ "result_url" => "https://your-app.com/result",
552
+ "queue_timeout_url" => "https://your-app.com/timeout"
553
+ })
97
554
  ```
98
- for Till Number push
555
+
556
+ **Account Balance Callbacks:**
557
+
558
+ Account balance queries send results to `result_url` and `queue_timeout_url`.
559
+
560
+ **Sample Result Callback Payload:**
99
561
  ```ruby
100
- 2.5.3 :010 > MpesaStk::Push.buy_goods('05', "<YOUR PHONE NUMBER: 254711222333>", hash)
562
+ {
563
+ "Result" => {
564
+ "ResultType" => 0,
565
+ "ResultCode" => 0,
566
+ "ResultDesc" => "The service request is processed successfully.",
567
+ "OriginatorConversationID" => "12345-67890-1",
568
+ "ConversationID" => "AG_20200101_00001234567890",
569
+ "ResultParameters" => {
570
+ "ResultParameter" => [
571
+ {"Key" => "AccountBalance", "Value" => "Working Account|KES|50000.00|50000.00|0.00|0.00"},
572
+ {"Key" => "BOCompletedTime", "Value" => "20200101120000"}
573
+ ]
574
+ }
575
+ }
576
+ }
101
577
  ```
102
- possible error format if the request is not successful
103
- ```hash
104
- {"requestId"=>"13022-8633727-1", "errorCode"=>"500.001.1001", "errorMessage"=>"Error Message"}
578
+
579
+ ### Reversal
580
+
581
+ Reverse a previously completed M-Pesa transaction. Useful for refunds, correcting errors, or handling disputes. The reversal amount must match the original transaction amount. Results are sent asynchronously to your ResultURL.
582
+
583
+ **Using ENV variables:**
584
+ ```ruby
585
+ # Set ENV variables: initiator, security_credential, result_url, queue_timeout_url
586
+ result = MpesaStk::Reversal.reverse("OFR4Z5EE9Y", "100", {})
587
+ ```
588
+
589
+ **Using hash parameters:**
590
+ ```ruby
591
+ result = MpesaStk::Reversal.reverse("OFR4Z5EE9Y", "100", {
592
+ "key" => "your_key",
593
+ "secret" => "your_secret",
594
+ "initiator" => "testapi",
595
+ "security_credential" => "encrypted_credential",
596
+ "receiver_party" => "174379", # optional, defaults to business_short_code
597
+ "receiver_identifier_type" => "4", # optional, defaults to "4"
598
+ "result_url" => "https://your-app.com/result",
599
+ "queue_timeout_url" => "https://your-app.com/timeout"
600
+ })
105
601
  ```
106
602
 
107
- expected irb output after the command
108
- ```hash
603
+ **Reversal Callbacks:**
604
+
605
+ Reversal requests send results to `result_url` and `queue_timeout_url`.
606
+
607
+ **Sample Result Callback Payload:**
608
+ ```ruby
609
+ {
610
+ "Result" => {
611
+ "ResultType" => 0,
612
+ "ResultCode" => 0,
613
+ "ResultDesc" => "The service request is processed successfully.",
614
+ "OriginatorConversationID" => "12345-67890-1",
615
+ "ConversationID" => "AG_20200101_00001234567890",
616
+ "TransactionID" => "LGR123456789",
617
+ "ResultParameters" => {
618
+ "ResultParameter" => [
619
+ {"Key" => "TransactionAmount", "Value" => "100.00"},
620
+ {"Key" => "TransactionReceipt", "Value" => "LGR123456789"},
621
+ {"Key" => "B2CWorkingAccountAvailableFunds", "Value" => "50000.00"},
622
+ {"Key" => "B2CUtilityAccountAvailableFunds", "Value" => "100000.00"},
623
+ {"Key" => "TransactionCompletedDateTime", "Value" => "01.01.2020 12:00:00"}
624
+ ]
625
+ }
626
+ }
627
+ }
628
+ ```
629
+
630
+ ### M-Pesa Ratiba (Standing Orders)
631
+
632
+ Create standing orders (recurring payments) that automatically charge customers at specified intervals. Perfect for subscriptions, monthly fees, or any recurring billing. Customers authorize the standing order once, and payments are automatically processed according to the frequency you set (daily, weekly, monthly, etc.).
633
+
634
+ **Using ENV variables:**
635
+ ```ruby
636
+ # Set ENV variables: business_short_code, callback_url
637
+ # Monthly subscription (Pay Bill)
638
+ result = MpesaStk::Ratiba.create_standing_order({
639
+ "standing_order_name" => "Monthly Subscription",
640
+ "amount" => "500",
641
+ "party_a" => "254712345678",
642
+ "frequency" => "3", # 1=Daily, 2=Weekly, 3=Monthly, 4=Bi-Monthly, 5=Quarterly, 6=Half-Year, 7=Yearly
643
+ "start_date" => "2025-09-25",
644
+ "end_date" => "2026-09-25",
645
+ "account_reference" => "SUB001",
646
+ "transaction_desc" => "Monthly subscription payment"
647
+ })
648
+
649
+ # For Buy Goods (Till Number)
650
+ result = MpesaStk::Ratiba.create_standing_order({
651
+ "amount" => "500",
652
+ "party_a" => "254712345678",
653
+ "transaction_type" => "Standing Order Customer Pay Merchant",
654
+ "receiver_party_identifier_type" => "2", # "2" for till number
655
+ "frequency" => "3",
656
+ "start_date" => "2025-09-25",
657
+ "end_date" => "2026-09-25"
658
+ })
659
+ ```
660
+
661
+ **Using hash parameters:**
662
+ ```ruby
663
+ # Monthly subscription (Pay Bill)
664
+ result = MpesaStk::Ratiba.create_standing_order({
665
+ "key" => "your_key",
666
+ "secret" => "your_secret",
667
+ "business_short_code" => "174379",
668
+ "standing_order_name" => "Monthly Subscription",
669
+ "amount" => "500",
670
+ "party_a" => "254712345678",
671
+ "frequency" => "3",
672
+ "start_date" => "2025-09-25",
673
+ "end_date" => "2026-09-25",
674
+ "account_reference" => "SUB001",
675
+ "transaction_desc" => "Monthly subscription payment",
676
+ "callback_url" => "https://your-app.com/callback"
677
+ })
678
+
679
+ # For Buy Goods (Till Number)
680
+ result = MpesaStk::Ratiba.create_standing_order({
681
+ "key" => "your_key",
682
+ "secret" => "your_secret",
683
+ "business_short_code" => "300584",
684
+ "amount" => "500",
685
+ "party_a" => "254712345678",
686
+ "transaction_type" => "Standing Order Customer Pay Merchant",
687
+ "receiver_party_identifier_type" => "2",
688
+ "frequency" => "3",
689
+ "start_date" => "2025-09-25",
690
+ "end_date" => "2026-09-25",
691
+ "callback_url" => "https://your-app.com/callback"
692
+ })
693
+ ```
694
+
695
+ **Note:** Ratiba (Standing Orders) uses the same callback structure as STK Push. When a standing order payment is processed, you'll receive a callback on your `callback_url` with the transaction status.
696
+
697
+ ### IoT APIs
698
+
699
+ Manage IoT SIM cards and send/receive messages for IoT devices. These APIs allow you to monitor, activate, and manage SIM cards used in IoT deployments, as well as send and receive SMS messages from IoT devices.
700
+
701
+ **SIM Operations:**
702
+
703
+ Manage and query information about IoT SIM cards including activation status, lifecycle, customer information, location, and subscription management.
704
+
705
+ **Using ENV variables:**
706
+ ```ruby
707
+ # Set ENV variables: iot_api_key, vpn_group, username
708
+ iot = MpesaStk::IoT.sims({})
709
+
710
+ # Get all SIMs
711
+ result = iot.get_all_sims(start_at_index: 0, page_size: 10)
712
+
713
+ # Query lifecycle status
714
+ result = iot.query_lifecycle_status("0110100606")
715
+
716
+ # Query customer info
717
+ result = iot.query_customer_info("0110100606")
718
+
719
+ # Activate SIM
720
+ result = iot.sim_activation("0110100606")
721
+
722
+ # Get activation trends
723
+ result = iot.get_activation_trends(start_date: "2025-01-01", stop_date: "2025-12-31")
724
+
725
+ # Rename asset
726
+ result = iot.rename_asset("0110100606", "New Asset Name")
727
+
728
+ # Get location info
729
+ result = iot.get_location_info("0110100606")
730
+
731
+ # Suspend/Unsuspend subscription
732
+ result = iot.suspend_unsuspend_sub("0110100606", "product_name", "suspend") # or "unsuspend"
733
+ ```
734
+
735
+ **Using hash parameters:**
736
+ ```ruby
737
+ iot = MpesaStk::IoT.sims({
738
+ "key" => "your_key",
739
+ "secret" => "your_secret",
740
+ "iot_api_key" => "Yl4S3KEcr173mbeUdYdjf147IuG3rJ824ArMkP6Z",
741
+ "vpn_group" => "your_vpn_group",
742
+ "username" => "your_username"
743
+ })
744
+
745
+ # All operations work the same way
746
+ result = iot.get_all_sims(start_at_index: 0, page_size: 10)
747
+ result = iot.query_lifecycle_status("0110100606")
748
+ # ... etc
749
+ ```
750
+
751
+ **Messaging Operations:**
752
+
753
+ Send and manage SMS messages to/from IoT devices. Includes functionality to send messages, search message history, filter messages by date/status, and manage message threads.
754
+
755
+ **Using ENV variables:**
756
+ ```ruby
757
+ # Set ENV variables: iot_api_key, vpn_group, username
758
+ messaging = MpesaStk::IoT.messaging({})
759
+
760
+ # Get all messages
761
+ result = messaging.get_all_messages(page_no: 1, page_size: 10)
762
+
763
+ # Search messages
764
+ result = messaging.search_messages("search_term", page_no: 1, page_size: 5)
765
+
766
+ # Filter messages
767
+ result = messaging.filter_messages(
768
+ start_date: "2025-01-01",
769
+ end_date: "2025-12-31",
770
+ status: "sent", # optional
771
+ page_no: 1,
772
+ page_size: 10
773
+ )
774
+
775
+ # Send single message
776
+ result = messaging.send_single_message("0110100606", "Hello from API")
777
+
778
+ # Delete message
779
+ result = messaging.delete_message(123)
780
+
781
+ # Delete message thread
782
+ result = messaging.delete_message_thread("0110100606")
783
+ ```
784
+
785
+ **Using hash parameters:**
786
+ ```ruby
787
+ messaging = MpesaStk::IoT.messaging({
788
+ "key" => "your_key",
789
+ "secret" => "your_secret",
790
+ "iot_api_key" => "Yl4S3KEcr173mbeUdYdjf147IuG3rJ824ArMkP6Z",
791
+ "vpn_group" => "your_vpn_group",
792
+ "username" => "your_username"
793
+ })
794
+
795
+ # All operations work the same way
796
+ result = messaging.get_all_messages(page_no: 1, page_size: 10)
797
+ result = messaging.send_single_message("0110100606", "Hello from API")
798
+ # ... etc
799
+ ```
800
+
801
+ ### IMSI/SWAP Operations
802
+
803
+ Query International Mobile Subscriber Identity (IMSI) and SIM Swap information for a phone number. Useful for fraud prevention, verifying SIM card authenticity, and checking if a SIM card has been swapped recently. Available in both v1 and v2 API versions.
804
+
805
+ **Using ENV variables:**
806
+ ```ruby
807
+ # Check ATI (v1)
808
+ result = MpesaStk::IMSI.check_ati("254712345678", {})
809
+
810
+ # Check ATI (v2)
811
+ result = MpesaStk::IMSI.check_ati("254712345678", {}, version: "v2")
812
+ ```
813
+
814
+ **Using hash parameters:**
815
+ ```ruby
816
+ # Check ATI (v1)
817
+ result = MpesaStk::IMSI.check_ati("254712345678", {
818
+ "key" => "your_key",
819
+ "secret" => "your_secret"
820
+ })
821
+
822
+ # Check ATI (v2)
823
+ result = MpesaStk::IMSI.check_ati("254712345678", {
824
+ "key" => "your_key",
825
+ "secret" => "your_secret"
826
+ }, version: "v2")
827
+ ```
828
+
829
+ ### Pull Transactions
830
+
831
+ Retrieve historical transaction data from your PayBill or Till Number. This API allows you to pull transaction records for reconciliation, reporting, or analysis purposes.
832
+
833
+ **Register Pull URL:**
834
+
835
+ Register a callback URL where Safaricom will send transaction data when you query for transactions. This is a one-time setup required before querying transactions.
836
+
837
+ **Using ENV variables:**
838
+ ```ruby
839
+ # Set ENV variables: callback_url
840
+ result = MpesaStk::PullTransactions.register({
841
+ "request_type" => "pull",
842
+ "nominated_number" => "254712345678"
843
+ })
844
+ ```
845
+
846
+ **Using hash parameters:**
847
+ ```ruby
848
+ result = MpesaStk::PullTransactions.register({
849
+ "key" => "your_key",
850
+ "secret" => "your_secret",
851
+ "short_code" => "174379",
852
+ "request_type" => "pull",
853
+ "nominated_number" => "254712345678",
854
+ "callback_url" => "https://your-app.com/pull_callback"
855
+ })
856
+ ```
857
+
858
+ **Query Transactions:**
859
+
860
+ Query and retrieve transaction records for a specific date range. Returns transaction details including amounts, phone numbers, transaction IDs, and timestamps. Results are sent asynchronously to your registered callback URL.
861
+
862
+ **Using ENV variables:**
863
+ ```ruby
864
+ # Set ENV variables: business_short_code
865
+ result = MpesaStk::PullTransactions.query(
866
+ "2020-08-04 8:36:00",
867
+ "2020-08-16 10:10:00",
868
+ {
869
+ "offset_value" => "0" # optional
870
+ }
871
+ )
872
+ ```
873
+
874
+ **Using hash parameters:**
875
+ ```ruby
876
+ result = MpesaStk::PullTransactions.query(
877
+ "2020-08-04 8:36:00",
878
+ "2020-08-16 10:10:00",
109
879
  {
110
- "MerchantRequestID"=>"7909-1302368-1",
111
- "CheckoutRequestID"=>"ws_CO_DMZ_40472724_16062018092359957",
112
- "ResponseCode"=>"0",
113
- "ResponseDescription"=>"Success. Request accepted for processing",
114
- "CustomerMessage"=>"Success. Request accepted for processing"
880
+ "key" => "your_key",
881
+ "secret" => "your_secret",
882
+ "short_code" => "174379",
883
+ "offset_value" => "0" # optional
115
884
  }
885
+ )
116
886
  ```
117
887
 
118
- the above response means the response has been successfully sent to Safaricom for processing and you should be able to see the checkout/express prompt on the sender number.
888
+ **Pull Transactions Callbacks:**
119
889
 
120
- ### Mpesa Checkout/Express
121
- This is the expected output on the mobile phone
890
+ Pull transaction queries send results to the registered `callback_url`.
122
891
 
123
- ![alt tag](./bin/index.jpeg)
892
+ **Sample Callback Payload:**
893
+ ```ruby
894
+ {
895
+ "Result" => {
896
+ "ResultType" => 0,
897
+ "ResultCode" => 0,
898
+ "ResultDesc" => "The service request is processed successfully.",
899
+ "OriginatorConversationID" => "12345-67890-1",
900
+ "ConversationID" => "AG_20200101_00001234567890",
901
+ "ResultParameters" => {
902
+ "ResultParameter" => [
903
+ {
904
+ "Key" => "TransactionDetails",
905
+ "Value" => "TransactionID|TransactionTime|Amount|MSISDN|FirstName|MiddleName|LastName|BusinessShortCode|BillRefNumber|InvoiceNumber|ThirdPartyTransID|TransactionStatus|TransactionType|OrgAccountBalance"
906
+ }
907
+ ]
908
+ }
909
+ }
910
+ }
911
+ ```
124
912
 
125
- ### Callback url
913
+ ## Handling Callbacks in Your Application
126
914
 
127
- After the pin code is entered on the checkout/express prompt. you will receive a request on the provided `callback_url` with the status of the action
915
+ **Rails Example:**
916
+ ```ruby
917
+ # config/routes.rb
918
+ post '/mpesa/callback', to: 'mpesa#stk_callback'
919
+ post '/mpesa/result', to: 'mpesa#result_callback'
920
+ post '/mpesa/timeout', to: 'mpesa#timeout_callback'
921
+ post '/mpesa/confirmation', to: 'mpesa#c2b_confirmation'
922
+
923
+ # app/controllers/mpesa_controller.rb
924
+ class MpesaController < ApplicationController
925
+ skip_before_action :verify_authenticity_token, only: [:stk_callback, :result_callback, :timeout_callback, :c2b_confirmation]
926
+
927
+ def stk_callback
928
+ callback_data = JSON.parse(request.body.read)
929
+ result_code = callback_data.dig('Body', 'stkCallback', 'ResultCode')
930
+
931
+ if result_code == 0
932
+ # Transaction successful
933
+ metadata = callback_data.dig('Body', 'stkCallback', 'CallbackMetadata', 'Item')
934
+ amount = metadata.find { |item| item['Name'] == 'Amount' }['Value']
935
+ receipt = metadata.find { |item| item['Name'] == 'MpesaReceiptNumber' }['Value']
936
+ # Process successful payment
937
+ else
938
+ # Transaction failed
939
+ result_desc = callback_data.dig('Body', 'stkCallback', 'ResultDesc')
940
+ # Handle failure
941
+ end
942
+
943
+ render json: { status: 'received' }, status: :ok
944
+ end
945
+
946
+ def result_callback
947
+ result_data = JSON.parse(request.body.read)
948
+ result_code = result_data.dig('Result', 'ResultCode')
949
+
950
+ if result_code == 0
951
+ # Process successful result
952
+ else
953
+ # Handle error
954
+ end
955
+
956
+ render json: { status: 'received' }, status: :ok
957
+ end
958
+
959
+ def timeout_callback
960
+ timeout_data = JSON.parse(request.body.read)
961
+ # Handle timeout
962
+ render json: { status: 'received' }, status: :ok
963
+ end
964
+
965
+ def c2b_confirmation
966
+ confirmation_data = JSON.parse(request.body.read)
967
+ trans_id = confirmation_data['TransID']
968
+ amount = confirmation_data['TransAmount']
969
+ # Process C2B payment confirmation
970
+ render json: { status: 'received' }, status: :ok
971
+ end
972
+ end
973
+ ```
974
+
975
+ **Sinatra Example:**
976
+ ```ruby
977
+ require 'sinatra'
978
+ require 'json'
979
+
980
+ post '/mpesa/callback' do
981
+ callback_data = JSON.parse(request.body.read)
982
+ # Process callback
983
+ status 200
984
+ { status: 'received' }.to_json
985
+ end
986
+ ```
987
+
988
+ **Best Practices:**
989
+
990
+ 1. **Always Return 200 OK**: Your callback endpoint must return HTTP 200 OK within 30 seconds
991
+ 2. **Idempotency**: Use `TransactionID` or `CheckoutRequestID` to prevent duplicate processing
992
+ 3. **Logging**: Log all callbacks for debugging and audit purposes
993
+ 4. **Error Handling**: Handle network issues and retries gracefully
994
+ 5. **Security**: Validate callback authenticity (consider IP whitelisting or signature verification)
995
+ 6. **Queue Processing**: Use background jobs for processing callbacks to avoid timeouts
996
+
997
+ ## Response Format & Error Handling
998
+
999
+ ### Success Response Format
1000
+
1001
+ All API methods return a hash with the response. Success responses typically include:
1002
+ - `ResponseCode`: "0" indicates success
1003
+ - `ResponseDescription`: Human-readable description
1004
+ - `MerchantRequestID` / `OriginatorConversationID`: Request identifiers
1005
+ - `CheckoutRequestID` / `ConversationID`: Transaction identifiers
128
1006
 
129
- sample payload that you will be getting on your callback
130
- ```hash
131
- {"Body"=>{"stkCallback"=>{"MerchantRequestID"=>"3968-94214-1", "CheckoutRequestID"=>"ws_CO_160620191218268004", "ResultCode"=>0, "ResultDesc"=>"The service request is processed successfully.",
132
- "CallbackMetadata"=>{"Item"=>[{"Name"=>"Amount", "Value"=>"05"}, {"Name"=>"MpesaReceiptNumber", "Value"=>"OFG4Z5EE9Y"}, {"Name"=>"TransactionDate", "Value"=>20190616121848},
133
- {"Name"=>"PhoneNumber", "Value"=>254711222333}]}}}, "push"=>{"Body"=>{"stkCallback"=>{"MerchantRequestID"=>"3968-94214-1", "CheckoutRequestID"=>"ws_CO_160620191218268004", "ResultCode"=>0,
134
- "ResultDesc"=>"The service request is processed successfully.", "CallbackMetadata"=>{"Item"=>[{"Name"=>"Amount", "Value"=>"05"}, {"Name"=>"MpesaReceiptNumber", "Value"=>"OFG4Z5EE9Y"}, {"Name"=>"TransactionDate",
135
- "Value"=>20190616121848}, {"Name"=>"PhoneNumber", "Value"=>254711222333}]}}}}}
1007
+ **Example Success Response:**
1008
+ ```ruby
1009
+ {
1010
+ "MerchantRequestID" => "7909-1302368-1",
1011
+ "CheckoutRequestID" => "ws_CO_DMZ_40472724_16062018092359957",
1012
+ "ResponseCode" => "0",
1013
+ "ResponseDescription" => "Success. Request accepted for processing",
1014
+ "CustomerMessage" => "Success. Request accepted for processing"
1015
+ }
1016
+ ```
1017
+
1018
+ ### Error Response Format
1019
+
1020
+ Error responses include:
1021
+ - `errorCode`: Error code (e.g., "500.001.1001")
1022
+ - `errorMessage`: Error description
1023
+ - `requestId`: Request identifier
1024
+
1025
+ **Example Error Response:**
1026
+ ```ruby
1027
+ {
1028
+ "requestId" => "13022-8633727-1",
1029
+ "errorCode" => "500.001.1001",
1030
+ "errorMessage" => "Error Message"
1031
+ }
1032
+ ```
1033
+
1034
+ ### Error Handling
1035
+
1036
+ All methods raise `StandardError` with descriptive messages when:
1037
+ - HTTP requests fail
1038
+ - Required configuration is missing
1039
+ - API returns error responses
1040
+
1041
+ ```ruby
1042
+ begin
1043
+ result = MpesaStk::PushPayment.call("500", "254712345678")
1044
+ rescue StandardError => e
1045
+ puts "Error: #{e.message}"
1046
+ end
136
1047
  ```
137
1048
 
138
1049
  ## Development
139
1050
 
140
- After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
1051
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run the tests:
1052
+
1053
+ ```bash
1054
+ # Run all tests
1055
+ ruby -Itest test/*_test.rb
1056
+
1057
+ # Or run specific test file
1058
+ ruby -Itest test/access_token_test.rb
1059
+ ```
1060
+
1061
+ You can also run `bin/console` for an interactive prompt that will allow you to experiment.
1062
+
1063
+ **Note:** If you encounter bundler deprecation warnings, update bundler with `gem install bundler` (requires bundler 2.4+ for Ruby 3.3+).
141
1064
 
142
1065
  To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
143
1066