mpesa_stk 1.3 → 3.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,163 +1,602 @@
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.
3
2
 
4
- [![Gem Version](https://badge.fury.io/rb/mpesa_stk.svg)](https://badge.fury.io/rb/mpesa_stk.svg)
5
- ![Cop](https://github.com/mboya/mpesa_stk/workflows/Cop/badge.svg?branch=master)
3
+ Ruby client for [Safaricom Daraja](https://developer.safaricom.co.ke/) APIs: Lipa na M-Pesa (STK Push), B2C/B2B/C2B, transaction queries, standing orders (Ratiba), IoT SIM portal, IMSI checks, and pull transactions.
4
+
5
+ [![Gem Version](https://badge.fury.io/rb/mpesa_stk.svg)](https://badge.fury.io/rb/mpesa_stk)
6
+ [![Cop](https://github.com/mboya/mpesa_stk/actions/workflows/cop.yml/badge.svg?branch=master)](https://github.com/mboya/mpesa_stk/actions/workflows/cop.yml)
7
+
8
+ **Version 3.0** — unified `.call` API with keyword options. Upgrading from 2.x? See [MIGRATION.md](MIGRATION.md).
9
+
10
+ **Requirements:** Ruby >= 2.6, [Redis](https://redis.io/) (OAuth tokens are cached per consumer key)
6
11
 
7
12
  ## Installation
8
13
 
9
- Add this line to your application's Gemfile:
14
+ Add to your Gemfile:
10
15
 
11
16
  ```ruby
12
17
  gem 'mpesa_stk'
13
18
  ```
14
- and run the `bundle install` command
15
19
 
16
- Or install it yourself as:
17
- ```ruby
20
+ Then:
21
+
22
+ ```bash
23
+ bundle install
24
+ ```
25
+
26
+ Or install directly:
27
+
28
+ ```bash
18
29
  gem install mpesa_stk
19
30
  ```
20
31
 
21
- # Getting Started
22
- This gem has a [Redis](https://redis.io/) dependency, so make sure it running
32
+ ## Configuration
33
+
34
+ Copy the sample environment file and fill in your Daraja credentials:
35
+
36
+ ```bash
37
+ cp .sample.env .env
38
+ ```
39
+
40
+ `require 'mpesa_stk'` loads `.env` via Dotenv when present (skip with `ENV['MPESA_STK_SKIP_DOTENV'] = 'true'` in Rails and similar apps).
41
+
42
+ Override settings in code:
43
+
23
44
  ```ruby
24
- $ redis-server
45
+ MpesaStk.configure do |c|
46
+ c[:key] = 'your_consumer_key'
47
+ c[:secret] = 'your_consumer_secret'
48
+ c[:business_short_code] = '174379'
49
+ c[:business_passkey] = 'your_passkey'
50
+ c[:callback_url] = 'https://your.app/mpesa/callback'
51
+ end
25
52
  ```
26
- You can use command line to determine if redis is running:
53
+
54
+ | Variable | Purpose |
55
+ |----------|---------|
56
+ | `base_url` | API host (sandbox or production) |
57
+ | `key`, `secret` | Consumer key and secret |
58
+ | `business_short_code`, `business_passkey` | STK / PayBill credentials |
59
+ | `callback_url` | STK Push callback |
60
+ | `confirmation_url` | C2B confirmation URL |
61
+ | `till_number` | Buy Goods till |
62
+ | `initiator`, `initiator_name`, `security_credential` | B2C/B2B/status/balance/reversal |
63
+ | `result_url`, `queue_timeout_url` | Async result endpoints |
64
+ | `iot_api_key`, `vpn_group`, `username` | IoT portal (optional) |
65
+
66
+ Endpoint paths (`process_request_url`, `b2c_url`, etc.) default to sandbox values in `.sample.env`. Change `base_url` and paths when moving to production.
67
+
68
+ **Redis** must be running locally (default `redis://127.0.0.1:6379`). `MpesaStk::AccessToken` stores and refreshes bearer tokens automatically.
69
+
70
+ ## Quick start
71
+
72
+ STK Push (PayBill) — one line when `.env` is configured:
73
+
27
74
  ```ruby
28
- redis-cli ping
75
+ require 'mpesa_stk'
76
+
77
+ response = MpesaStk::Push.call(100, '254712345678')
78
+ # => { "ResponseCode" => "0", "CheckoutRequestID" => "ws_CO_...", ... }
29
79
  ```
30
- you should get back
80
+
81
+ Register your `callback_url` and handle the STK result on your server ([STK Push](#stk-push)).
82
+
83
+ ## Usage
84
+
85
+ Every client exposes a **single entry point**:
86
+
87
+ - `.call(...)` for payments and queries
88
+ - `.register(...)` for URL registration (C2B, pull transactions)
89
+
90
+ Pass **keyword options** to override `.env` for a request (credentials, short codes, URLs):
91
+
31
92
  ```ruby
32
- PONG
93
+ MpesaStk::Push.call(
94
+ 100,
95
+ '254712345678',
96
+ business_short_code: '174379',
97
+ business_passkey: 'your_passkey',
98
+ callback_url: 'https://your.app/callback',
99
+ key: 'consumer_key',
100
+ secret: 'consumer_secret'
101
+ )
33
102
  ```
34
103
 
35
- you need to setup your environment variables, checkout `.sample.env` for the values you need.
36
- or run
104
+ | Intent | Call |
105
+ |--------|------|
106
+ | STK PayBill (default) | `Push.call(amount, phone)` |
107
+ | STK Buy Goods | `Push.call(amount, phone, type: :buy_goods)` |
108
+ | B2C payout | `B2C.call(amount, phone)` |
109
+ | STK status | `StkPushQuery.call(checkout_request_id)` |
110
+
111
+ ## Understanding responses
112
+
113
+ Every successful API call returns a **parsed `Hash`** (JSON keys as strings). In most Daraja APIs, `"ResponseCode" => "0"` means the request was **accepted for processing**, not that money has already moved.
114
+
115
+ | Layer | Who delivers it | Your action |
116
+ |-------|-----------------|-------------|
117
+ | **Synchronous** | Returned by the gem immediately | Store IDs (`CheckoutRequestID`, `ConversationID`, etc.) |
118
+ | **Callback** | Safaricom `POST` to your HTTPS URL | Update orders, wallets, logs; always respond HTTP 200 |
119
+ | **Query** | Optional follow-up (`StkPushQuery`, pull API) | Poll when you did not receive a callback |
120
+
121
+ On HTTP errors the gem raises `StandardError` with status and body, for example:
122
+
123
+ ```json
124
+ {
125
+ "errorCode": "400.001.1001",
126
+ "errorMessage": "Bad Request"
127
+ }
128
+ ```
129
+
130
+ Sandbox and production payloads follow the same shape; values differ. Confirm against [official Daraja docs](https://developer.safaricom.co.ke/Documentation) when integrating a new API version.
131
+
132
+ ## API reference
133
+
134
+ | Class | Entry point | Notes |
135
+ |-------|-------------|--------|
136
+ | `MpesaStk::Push` | `.call(amount, phone, type: :pay_bill \| :buy_goods, **options)` | STK Push; PayBill is default |
137
+ | `MpesaStk::StkPushQuery` | `.call(checkout_request_id, **options)` | STK payment status |
138
+ | `MpesaStk::B2C` | `.call(amount, phone, **options)` | Business → customer |
139
+ | `MpesaStk::B2B` | `.call(amount, receiver_party, **options)` | Business → business |
140
+ | `MpesaStk::C2B` | `.register(**options)`, `.call(amount, phone, **options)` | URL registration & sandbox simulate |
141
+ | `MpesaStk::TransactionStatus` | `.call(transaction_id, **options)` | Transaction status query |
142
+ | `MpesaStk::AccountBalance` | `.call(**options)` | Balance (async via `result_url`) |
143
+ | `MpesaStk::Reversal` | `.call(transaction_id, amount, **options)` | Reverse a transaction |
144
+ | `MpesaStk::Ratiba` | `.call(amount:, party_a:, start_date:, end_date:, **options)` | Standing orders |
145
+ | `MpesaStk::PullTransactions` | `.register(**options)`, `.call(start, end, **options)` | Pull transactions |
146
+ | `MpesaStk::IMSI` | `.call(customer_number, version: 'v1', **options)` | IMSI / SIM swap check |
147
+ | `MpesaStk::IoT` | `.list_sims`, `.send_message`, `.call(:method, ...)` | IoT SIM portal |
148
+ | `MpesaStk::AccessToken` | `.call(key, secret)` | OAuth token (usually internal) |
149
+
150
+ Phone numbers use international format without `+`, e.g. `254712345678`.
151
+
152
+ ---
153
+
154
+ ### STK Push (`MpesaStk::Push`)
155
+
156
+ `MpesaStk::Push` replaces the former `PushPayment` and `Push` classes from 2.x.
157
+
158
+ **PayBill** (default) — uses `business_short_code` as `PartyB`:
159
+
37
160
  ```ruby
38
- $ cp .sample.env .env
161
+ MpesaStk::Push.call(100, '254712345678')
39
162
  ```
40
- open `.env` on your editor and add the missing variable
163
+
164
+ **Buy Goods** — requires `till_number` in `.env` or as a keyword:
165
+
166
+ ```ruby
167
+ MpesaStk::Push.call(100, '254712345678', type: :buy_goods)
168
+ MpesaStk::Push.call(100, '254712345678', type: :buy_goods, till_number: '174379')
41
169
  ```
42
- key=""
43
- secret=""
44
- business_short_code=""
45
- business_passkey=""
46
- callback_url=""
47
- till_number=""
170
+
171
+ **Per-request overrides** (multi-app or custom credentials):
172
+
173
+ ```ruby
174
+ MpesaStk::Push.call(
175
+ 100,
176
+ '254712345678',
177
+ business_short_code: '174379',
178
+ business_passkey: 'your_passkey',
179
+ callback_url: 'https://your.app/callback',
180
+ key: 'consumer_key',
181
+ secret: 'consumer_secret'
182
+ )
48
183
  ```
49
184
 
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`
185
+ **Immediate response**:
53
186
 
54
- `Prod:`
187
+ ```json
188
+ {
189
+ "MerchantRequestID": "7909-1302368-1",
190
+ "CheckoutRequestID": "ws_CO_DMZ_40472724_16062018092359957",
191
+ "ResponseCode": "0",
192
+ "ResponseDescription": "Success. Request accepted for processing",
193
+ "CustomerMessage": "Success. Request accepted for processing"
194
+ }
195
+ ```
55
196
 
56
- when going live there information will be sent to your email.
197
+ Persist `CheckoutRequestID` to match the callback and STK query.
198
+
199
+ **Callback** (Safaricom `POST` to `callback_url` — not returned by the gem):
200
+
201
+ Success:
202
+
203
+ ```json
204
+ {
205
+ "Body": {
206
+ "stkCallback": {
207
+ "MerchantRequestID": "7909-1302368-1",
208
+ "CheckoutRequestID": "ws_CO_DMZ_40472724_16062018092359957",
209
+ "ResultCode": 0,
210
+ "ResultDesc": "The service request is processed successfully.",
211
+ "CallbackMetadata": {
212
+ "Item": [
213
+ { "Name": "Amount", "Value": 100 },
214
+ { "Name": "MpesaReceiptNumber", "Value": "QK4A1B2C3D" },
215
+ { "Name": "TransactionDate", "Value": 20250125143000 },
216
+ { "Name": "PhoneNumber", "Value": 254712345678 }
217
+ ]
218
+ }
219
+ }
220
+ }
221
+ }
222
+ ```
57
223
 
58
- for `buy_goods` push `business_short_code` will be equivalent to `store number` and `till_number` will remain as is.
224
+ Failed or cancelled (no `CallbackMetadata`; common `ResultCode` values: `1032` cancelled, `1037` timeout):
225
+
226
+ ```json
227
+ {
228
+ "Body": {
229
+ "stkCallback": {
230
+ "MerchantRequestID": "7909-1302368-1",
231
+ "CheckoutRequestID": "ws_CO_DMZ_40472724_16062018092359957",
232
+ "ResultCode": 1032,
233
+ "ResultDesc": "Request cancelled by user."
234
+ }
235
+ }
236
+ }
237
+ ```
59
238
 
60
- ### Testing out the gem in an actual Rails application
239
+ ---
61
240
 
62
- To test out the app on an actual rails application, do check out the following link:
241
+ ### STK Push query
63
242
 
64
- https://github.com/mboya/stk
243
+ ```ruby
244
+ MpesaStk::StkPushQuery.call('ws_CO_DMZ_40472724_16062018092359957')
245
+ ```
65
246
 
66
- ```shell
67
- https://github.com/mboya/stk
247
+ **Immediate response** (accepted query; payment outcome is in `ResultCode`):
248
+
249
+ ```json
250
+ {
251
+ "ResponseCode": "0",
252
+ "ResponseDescription": "The service request is processed successfully.",
253
+ "MerchantRequestID": "7909-1302368-1",
254
+ "CheckoutRequestID": "ws_CO_DMZ_40472724_16062018092359957",
255
+ "ResultCode": "0",
256
+ "ResultDesc": "The service request is processed successfully."
257
+ }
68
258
  ```
69
- #### Sample application
70
- Check out a rails sample application [here](https://github.com/mboya/stk)
71
259
 
72
- ### Testing the gem on the console/app
73
- When running the gem on a single safaricom app.
260
+ When the customer has not completed payment you may see non-zero `ResultCode` (e.g. `499` pending). Plan to combine query results with the callback for a single source of truth.
261
+
262
+ ---
263
+
264
+ ### B2C
74
265
 
75
266
  ```ruby
76
- $ irb
77
- 2.5.0 :001 > require 'mpesa_stk'
78
- 2.5.0 :002 > MpesaStk::PushPayment.call("500", "<YOUR PHONE NUMBER: 254711222333>")
267
+ MpesaStk::B2C.call(100, '254712345678', command_id: 'BusinessPayment', remarks: 'Salary')
268
+ ```
269
+
270
+ **Immediate response:**
271
+
272
+ ```json
273
+ {
274
+ "ResponseCode": "0",
275
+ "ResponseDescription": "The service request is processed successfully.",
276
+ "OriginatorConversationID": "12345-67890-1",
277
+ "ConversationID": "AG_20240101_12345678901234567890"
278
+ }
279
+ ```
280
+
281
+ **Callback** (`result_url` / `queue_timeout_url`):
282
+
283
+ ```json
284
+ {
285
+ "Result": {
286
+ "ResultType": 0,
287
+ "ResultCode": 0,
288
+ "ResultDesc": "The service request is processed successfully.",
289
+ "OriginatorConversationID": "12345-67890-1",
290
+ "ConversationID": "AG_20240101_12345678901234567890",
291
+ "TransactionID": "NLJ41HAAAA",
292
+ "ResultParameters": {
293
+ "ResultParameter": [
294
+ { "Key": "TransactionAmount", "Value": 100 },
295
+ { "Key": "TransactionReceipt", "Value": "NLJ41HAAAA" },
296
+ { "Key": "ReceiverPartyPublicName", "Value": "254712345678 - John Doe" },
297
+ { "Key": "TransactionDate", "Value": "25.1.2025 12:00:00 AM" }
298
+ ]
299
+ }
300
+ }
301
+ }
79
302
  ```
80
303
 
81
- When running the app on multiple safaricom apps, within the same project.
304
+ ---
305
+
306
+ ### B2B
307
+
82
308
  ```ruby
83
- $ 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
309
+ MpesaStk::B2B.call(500, '600000', command_id: 'BusinessPayBill', account_reference: 'INV-001')
310
+ ```
311
+
312
+ **Immediate response** (same shape as B2C):
313
+
314
+ ```json
315
+ {
316
+ "ResponseCode": "0",
317
+ "ResponseDescription": "The service request is processed successfully.",
318
+ "OriginatorConversationID": "12345-67890-1",
319
+ "ConversationID": "AG_20240101_12345678901234567890"
320
+ }
93
321
  ```
94
- for STK push
322
+
323
+ Final payment outcome arrives on `result_url` in the same `Result` wrapper format as B2C.
324
+
325
+ ---
326
+
327
+ ### C2B
328
+
95
329
  ```ruby
96
- 2.5.3 :010 > MpesaStk::Push.pay_bill('05', "<YOUR PHONE NUMBER: 254711222333>", hash)
330
+ MpesaStk::C2B.register(validation_url: 'https://your.app/validate')
331
+ MpesaStk::C2B.call(100, '254712345678', bill_ref_number: 'ORDER-1')
97
332
  ```
98
- for Till Number push
333
+
334
+ **Register URL — immediate response:**
335
+
336
+ ```json
337
+ {
338
+ "ResponseCode": "0",
339
+ "ResponseDescription": "success"
340
+ }
341
+ ```
342
+
343
+ **Simulate — immediate response:**
344
+
345
+ ```json
346
+ {
347
+ "ResponseCode": "0",
348
+ "ResponseDescription": "Accept the service request successfully."
349
+ }
350
+ ```
351
+
352
+ **Confirmation callback** (Safaricom `POST` to `confirmation_url` on real payments):
353
+
354
+ ```json
355
+ {
356
+ "TransactionType": "Pay Bill",
357
+ "TransID": "RKTQDM7W6S",
358
+ "TransTime": "20190608200106",
359
+ "TransAmount": "100.00",
360
+ "BusinessShortCode": "174379",
361
+ "BillRefNumber": "ORDER-1",
362
+ "InvoiceNumber": "",
363
+ "OrgAccountBalance": "49197.00",
364
+ "ThirdPartyTransID": "",
365
+ "MSISDN": "254712345678",
366
+ "FirstName": "John",
367
+ "MiddleName": "",
368
+ "LastName": "Doe"
369
+ }
370
+ ```
371
+
372
+ ---
373
+
374
+ ### Transaction status
375
+
99
376
  ```ruby
100
- 2.5.3 :010 > MpesaStk::Push.buy_goods('05', "<YOUR PHONE NUMBER: 254711222333>", hash)
377
+ MpesaStk::TransactionStatus.call('RKTQDM7W6S')
101
378
  ```
102
- possible error format if the request is not successful
103
- ```hash
104
- {"requestId"=>"13022-8633727-1", "errorCode"=>"500.001.1001", "errorMessage"=>"Error Message"}
379
+
380
+ **Immediate response:**
381
+
382
+ ```json
383
+ {
384
+ "ResponseCode": "0",
385
+ "ResponseDescription": "The service request is processed successfully.",
386
+ "ConversationID": "AG_20240101_12345678901234567890",
387
+ "OriginatorConversationID": "12345-67890-1"
388
+ }
105
389
  ```
106
390
 
107
- expected irb output after the command
108
- ```hash
109
- {
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"
391
+ **Result callback** (`result_url`) includes transaction details inside `Result.ResultParameters` (same pattern as B2C).
392
+
393
+ ---
394
+
395
+ ### Account balance
396
+
397
+ ```ruby
398
+ MpesaStk::AccountBalance.call
399
+ ```
400
+
401
+ **Immediate response:**
402
+
403
+ ```json
404
+ {
405
+ "ResponseCode": "0",
406
+ "ResponseDescription": "The service request is processed successfully.",
407
+ "OriginatorConversationID": "12345-67890-1",
408
+ "ConversationID": "AG_20240101_12345678901234567890"
409
+ }
410
+ ```
411
+
412
+ **Result callback** (`result_url`) — balance in `ResultParameters`:
413
+
414
+ ```json
415
+ {
416
+ "Result": {
417
+ "ResultType": 0,
418
+ "ResultCode": 0,
419
+ "ResultDesc": "The service request is processed successfully.",
420
+ "OriginatorConversationID": "12345-67890-1",
421
+ "ConversationID": "AG_20240101_12345678901234567890",
422
+ "ResultParameters": {
423
+ "ResultParameter": [
424
+ { "Key": "AccountBalance", "Value": "3080.00" },
425
+ { "Key": "BOCompletedTime", "Value": "20250125120000" }
426
+ ]
427
+ }
115
428
  }
429
+ }
430
+ ```
431
+
432
+ ---
433
+
434
+ ### Reversal
435
+
436
+ ```ruby
437
+ MpesaStk::Reversal.call('RKTQDM7W6S', 100)
116
438
  ```
117
439
 
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.
440
+ **Immediate response:**
119
441
 
120
- ### Mpesa Checkout/Express
121
- This is the expected output on the mobile phone
442
+ ```json
443
+ {
444
+ "ResponseCode": "0",
445
+ "ResponseDescription": "The service request is processed successfully.",
446
+ "OriginatorConversationID": "12345-67890-1",
447
+ "ConversationID": "AG_20240101_12345678901234567890"
448
+ }
449
+ ```
122
450
 
123
- ![alt tag](./bin/index.jpeg)
451
+ Outcome is confirmed on `result_url` via the standard `Result` object.
124
452
 
125
- ### Callback url
453
+ ---
126
454
 
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
455
+ ### Ratiba (standing orders)
128
456
 
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}]}}}}}
457
+ ```ruby
458
+ MpesaStk::Ratiba.call(
459
+ amount: 100,
460
+ party_a: '254712345678',
461
+ start_date: '2025-01-01',
462
+ end_date: '2025-12-31',
463
+ frequency: '3',
464
+ account_reference: 'SUB-001'
465
+ )
136
466
  ```
137
467
 
138
- ## Development
468
+ **Immediate response:**
139
469
 
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.
470
+ ```json
471
+ {
472
+ "ResponseCode": "0",
473
+ "ResponseDescription": "The service request is processed successfully.",
474
+ "OriginatorConversationID": "12345-67890-1",
475
+ "ConversationID": "AG_20240101_12345678901234567890"
476
+ }
477
+ ```
141
478
 
142
- 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).
479
+ Standing-order charge results are delivered to `callback_url` per Daraja Ratiba documentation.
143
480
 
144
- ## Contributing
481
+ ---
145
482
 
146
- Bug reports and pull requests are welcome on GitHub at https://github.com/mboya/mpesa_stk.
483
+ ### Pull transactions
147
484
 
148
- To Contribute to this gem,
149
- * Comment on the issue you would like to work on solving.
150
- * Mark the issue as in progress by adding an `in-progress` label.
151
- * Fork the project to your github repository (This project only accepts PRs from forks)
152
- * Submit the PR after the implementation all unfinished PRs for an issue should have a WIP indicated beside it
153
- * Every PR should have a link to the issue being solved
154
- * Checkout this [github best practices](https://github.com/skyscreamer/yoga/wiki/GitHub-Best-Practices) for more info.
485
+ ```ruby
486
+ MpesaStk::PullTransactions.register(request_type: 'Pull', nominated_number: '254712345678')
487
+ MpesaStk::PullTransactions.call('2025-01-01 00:00:00', '2025-01-31 23:59:59')
488
+ ```
155
489
 
156
- ## License
490
+ **Register — immediate response:**
491
+
492
+ ```json
493
+ {
494
+ "ResponseCode": "0",
495
+ "ResponseDescription": "success"
496
+ }
497
+ ```
498
+
499
+ **Query — immediate response**
500
+
501
+ The gem returns whatever JSON Safaricom sends (`JSON.parse` only). The repo does **not** include a captured production/sandbox payload for this endpoint. Tests stub a minimal shape:
502
+
503
+ ```json
504
+ {
505
+ "ResponseCode": "0",
506
+ "data": []
507
+ }
508
+ ```
509
+
510
+ Here `data: []` is a **placeholder** in tests meaning “success, list field exists”—not “the API never returns rows.” When transactions exist for the date range, expect objects inside `data` (field names per [Daraja pull-transactions docs](https://developer.safaricom.co.ke/)). Historical rows may also arrive on your registered `callback_url`.
511
+
512
+ ---
157
513
 
158
- The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
514
+ ### IMSI / SIM swap
159
515
 
160
- ## Code of Conduct
516
+ ```ruby
517
+ MpesaStk::IMSI.call('254712345678', version: 'v2')
518
+ ```
519
+
520
+ **Immediate response**
521
+
522
+ Same as above: the gem passes through the API body. Tests stub `{ "success": true, "data": {} }`—`data: {}` is a **placeholder**, not documentation of an empty real response. Live `checkATI` payloads (v1/v2) include swap/IMSI fields inside `data` per Safaricom’s spec. Use this to gate high-risk flows (e.g. extra verification after a recent SIM swap).
523
+
524
+ ---
525
+
526
+ ### IoT SIM portal
527
+
528
+ Shortcuts:
529
+
530
+ ```ruby
531
+ MpesaStk::IoT.list_sims(start_at_index: 0, page_size: 10)
532
+ MpesaStk::IoT.send_message('0110100606', 'Hello device')
533
+ ```
534
+
535
+ Or dispatch any instance method by name:
536
+
537
+ ```ruby
538
+ MpesaStk::IoT.call(:query_lifecycle_status, '0110100606')
539
+ MpesaStk::IoT.call(:search_messages, 'keyword', page_no: 1, page_size: 5)
540
+ ```
541
+
542
+ **List SIMs / messages**
543
+
544
+ Tests stub a minimal success body (again, **not** a guarantee of empty inventory):
545
+
546
+ ```json
547
+ {
548
+ "success": true,
549
+ "data": []
550
+ }
551
+ ```
552
+
553
+ With SIMs or messages present, `data` is typically an **array of objects** (MSISDN, status, pagination fields, etc.—per endpoint). Capture one response from your sandbox account and treat that as your reference.
161
554
 
162
- Everyone interacting in the MpesaStk project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/mboya/mpesa_stk/blob/master/CODE_OF_CONDUCT.md).
555
+ **Send message** tests only assert success; a plausible live shape might look like:
556
+
557
+ ```json
558
+ {
559
+ "success": true,
560
+ "data": {
561
+ "messageId": "msg-12345",
562
+ "status": "sent"
563
+ }
564
+ }
565
+ ```
566
+
567
+ That send-message example is **illustrative**; confirm against the IoT portal API docs for your environment.
568
+
569
+ Other IoT methods include `query_lifecycle_status`, `sim_activation`, `get_all_messages`, `filter_messages`, `delete_message`, and `delete_message_thread`. Responses come directly from the portal API and vary by endpoint.
570
+
571
+ ---
572
+
573
+ ## Development
574
+
575
+ ```bash
576
+ git clone https://github.com/mboya/mpesa_stk.git
577
+ cd mpesa_stk
578
+ bin/setup # bundle install
579
+ cp .sample.env .env
580
+ bundle exec rake test
581
+ bundle exec rubocop
582
+ ```
583
+
584
+ Use the [Safaricom sandbox](https://developer.safaricom.co.ke/) and valid test credentials in `.env` for manual checks.
585
+
586
+ ```bash
587
+ bundle exec rake test # 75 tests, WebMock (no live API calls)
588
+ bundle exec rubocop
589
+ ```
590
+
591
+ ## Contributing
592
+
593
+ 1. Fork the repository and create a feature branch.
594
+ 2. Add tests for new behaviour under `test/`.
595
+ 3. Run `bundle exec rake test` and `bundle exec rubocop`.
596
+ 4. Open a pull request with a clear description.
597
+
598
+ Please read [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md) before participating. Security issues: see [SECURITY.md](SECURITY.md). Release history: [CHANGELOG.md](CHANGELOG.md).
599
+
600
+ ## License
163
601
 
602
+ Copyright (c) 2018 mboya. Released under the [MIT License](LICENSE.txt).