mpesa_stk 2.0.0 → 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,219 +1,219 @@
1
1
  # MpesaStk
2
2
 
3
- Copyright (c) 2018 mboya
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
4
 
5
- MIT License
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)
6
7
 
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:
8
+ **Version 3.0** unified `.call` API with keyword options. Upgrading from 2.x? See [MIGRATION.md](MIGRATION.md).
13
9
 
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.
28
-
29
- [![Gem Version](https://badge.fury.io/rb/mpesa_stk.svg)](https://badge.fury.io/rb/mpesa_stk.svg)
30
- ![Cop](https://github.com/mboya/mpesa_stk/workflows/Cop/badge.svg?branch=master)
10
+ **Requirements:** Ruby >= 2.6, [Redis](https://redis.io/) (OAuth tokens are cached per consumer key)
31
11
 
32
12
  ## Installation
33
13
 
34
- Add this line to your application's Gemfile:
14
+ Add to your Gemfile:
35
15
 
36
16
  ```ruby
37
17
  gem 'mpesa_stk'
38
18
  ```
39
- and run the `bundle install` command
40
-
41
- Or install it yourself as:
42
- ```ruby
43
- gem install mpesa_stk
44
- ```
45
19
 
46
- # Getting Started
20
+ Then:
47
21
 
48
- ## Prerequisites
49
-
50
- **Redis:** This gem has a [Redis](https://redis.io/) dependency, so make sure it's running:
51
22
  ```bash
52
- $ redis-server
23
+ bundle install
53
24
  ```
54
25
 
55
- You can verify Redis is running:
26
+ Or install directly:
27
+
56
28
  ```bash
57
- $ redis-cli ping
58
- # Should return: PONG
29
+ gem install mpesa_stk
59
30
  ```
60
31
 
61
- **Bundler:** Ensure you have a recent version of bundler (2.4+ recommended for Ruby 3.3+):
32
+ ## Configuration
33
+
34
+ Copy the sample environment file and fill in your Daraja credentials:
35
+
62
36
  ```bash
63
- $ gem install bundler
64
- $ bundle --version # Should show 2.4.0 or higher
37
+ cp .sample.env .env
65
38
  ```
66
39
 
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.
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:
68
43
 
69
- you need to setup your environment variables, checkout `.sample.env` for the values you need.
70
- or run
71
44
  ```ruby
72
- $ cp .sample.env .env
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
73
52
  ```
74
- open `.env` on your editor and add the missing variables. See `.sample.env` for all available configuration options.
75
53
 
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)
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) |
80
65
 
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
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.
86
67
 
87
- **For C2B:**
88
- * `confirmation_url` - URL for payment confirmations
68
+ **Redis** must be running locally (default `redis://127.0.0.1:6379`). `MpesaStk::AccessToken` stores and refreshes bearer tokens automatically.
89
69
 
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
70
+ ## Quick start
94
71
 
95
- `Prod:`
72
+ STK Push (PayBill) — one line when `.env` is configured:
96
73
 
97
- when going live there information will be sent to your email.
74
+ ```ruby
75
+ require 'mpesa_stk'
98
76
 
99
- for `buy_goods` push `business_short_code` will be equivalent to `store number` and `till_number` will remain as is.
77
+ response = MpesaStk::Push.call(100, '254712345678')
78
+ # => { "ResponseCode" => "0", "CheckoutRequestID" => "ws_CO_...", ... }
79
+ ```
100
80
 
101
- ### Testing out the gem in an actual Rails application
81
+ Register your `callback_url` and handle the STK result on your server ([STK Push](#stk-push)).
102
82
 
103
- To test out the app on an actual rails application, do check out the following link:
83
+ ## Usage
104
84
 
105
- https://github.com/mboya/stk
85
+ Every client exposes a **single entry point**:
106
86
 
107
- ```shell
108
- https://github.com/mboya/stk
109
- ```
110
- #### Sample application
111
- Check out a rails sample application [here](https://github.com/mboya/stk)
87
+ - `.call(...)` for payments and queries
88
+ - `.register(...)` for URL registration (C2B, pull transactions)
112
89
 
113
- ### Quick Start Examples
90
+ Pass **keyword options** to override `.env` for a request (credentials, short codes, URLs):
114
91
 
115
- **Single App (using ENV variables):**
116
92
  ```ruby
117
- $ irb
118
- > require 'mpesa_stk'
119
- > MpesaStk::PushPayment.call("500", "254711222333")
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
+ )
120
102
  ```
121
103
 
122
- **Multiple Apps (using hash parameters):**
123
- ```ruby
124
- $ irb
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
- ```
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)` |
137
110
 
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.
111
+ ## Understanding responses
139
112
 
140
- ### Mpesa Checkout/Express
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.
141
114
 
142
- After initiating an STK Push, the customer will see a payment prompt on their phone. This is the expected output:
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 |
143
120
 
144
- ![alt tag](./bin/index.jpeg)
121
+ On HTTP errors the gem raises `StandardError` with status and body, for example:
145
122
 
146
- ### Callback URL Requirements
123
+ ```json
124
+ {
125
+ "errorCode": "400.001.1001",
126
+ "errorMessage": "Bad Request"
127
+ }
128
+ ```
147
129
 
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
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.
153
131
 
154
- ## API Reference
132
+ ## API reference
155
133
 
156
- ### STK Push (Lipa na M-Pesa Online)
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
+ ---
157
153
 
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.
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
159
 
160
- **Single App (using ENV variables):**
161
160
  ```ruby
162
- result = MpesaStk::PushPayment.call("500", "254712345678")
161
+ MpesaStk::Push.call(100, '254712345678')
163
162
  ```
164
163
 
165
- **Multiple Apps (using hash parameters):**
164
+ **Buy Goods** requires `till_number` in `.env` or as a keyword:
165
+
166
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
- }
167
+ MpesaStk::Push.call(100, '254712345678', type: :buy_goods)
168
+ MpesaStk::Push.call(100, '254712345678', type: :buy_goods, till_number: '174379')
169
+ ```
174
170
 
175
- # Pay Bill
176
- result = MpesaStk::Push.pay_bill("500", "254712345678", hash)
171
+ **Per-request overrides** (multi-app or custom credentials):
177
172
 
178
- # Buy Goods (Till Number)
179
- hash["till_number"] = "174379"
180
- result = MpesaStk::Push.buy_goods("500", "254712345678", hash)
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
+ )
181
183
  ```
182
184
 
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.
185
+ **Immediate response**:
186
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
- })
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
+ }
196
195
  ```
197
196
 
198
- **STK Push Callbacks:**
197
+ Persist `CheckoutRequestID` to match the callback and STK query.
199
198
 
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.
199
+ **Callback** (Safaricom `POST` to `callback_url` not returned by the gem):
201
200
 
202
- **Sample Callback Payload:**
203
- ```ruby
201
+ Success:
202
+
203
+ ```json
204
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}
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
217
  ]
218
218
  }
219
219
  }
@@ -221,866 +221,382 @@ After the customer enters their PIN on the checkout/express prompt, you will rec
221
221
  }
222
222
  ```
223
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:**
224
+ Failed or cancelled (no `CallbackMetadata`; common `ResultCode` values: `1032` cancelled, `1037` timeout):
249
225
 
250
- Transaction status queries send results to `result_url` and `queue_timeout_url`.
251
-
252
- **Sample Result Callback Payload:**
253
- ```ruby
226
+ ```json
254
227
  {
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
- ]
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."
272
234
  }
273
235
  }
274
236
  }
275
237
  ```
276
238
 
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.
239
+ ---
280
240
 
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
- ```
241
+ ### STK Push query
290
242
 
291
- **Using hash parameters:**
292
243
  ```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
- })
244
+ MpesaStk::StkPushQuery.call('ws_CO_DMZ_40472724_16062018092359957')
304
245
  ```
305
246
 
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
247
+ **Immediate response** (accepted query; payment outcome is in `ResultCode`):
311
248
 
312
- **Sample Result Callback Payload:**
313
- ```ruby
249
+ ```json
314
250
  {
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
- }
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."
340
257
  }
341
258
  ```
342
259
 
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
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.
352
261
 
353
- ### B2B (Business to Business)
262
+ ---
354
263
 
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.
264
+ ### B2C
356
265
 
357
- **Using ENV variables:**
358
266
  ```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
- })
267
+ MpesaStk::B2C.call(100, '254712345678', command_id: 'BusinessPayment', remarks: 'Salary')
371
268
  ```
372
269
 
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
- ```
270
+ **Immediate response:**
399
271
 
400
- **B2B Callbacks:**
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
+ ```
401
280
 
402
- Similar to B2C, B2B payments use `result_url` and `queue_timeout_url` for callbacks.
281
+ **Callback** (`result_url` / `queue_timeout_url`):
403
282
 
404
- **Sample Result Callback Payload:**
405
- ```ruby
283
+ ```json
406
284
  {
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"}
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" }
422
298
  ]
423
299
  }
424
300
  }
425
301
  }
426
302
  ```
427
303
 
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:**
304
+ ---
433
305
 
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.
306
+ ### B2B
435
307
 
436
- **Using ENV variables:**
437
308
  ```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
- })
309
+ MpesaStk::B2B.call(500, '600000', command_id: 'BusinessPayBill', account_reference: 'INV-001')
443
310
  ```
444
311
 
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
- })
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
+ }
455
321
  ```
456
322
 
457
- **Simulate Payment (Sandbox only):**
323
+ Final payment outcome arrives on `result_url` in the same `Result` wrapper format as B2C.
458
324
 
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.
325
+ ---
460
326
 
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
- ```
327
+ ### C2B
469
328
 
470
- **Using hash parameters:**
471
329
  ```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
- })
330
+ MpesaStk::C2B.register(validation_url: 'https://your.app/validate')
331
+ MpesaStk::C2B.call(100, '254712345678', bill_ref_number: 'ORDER-1')
479
332
  ```
480
333
 
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
334
+ **Register URL — immediate response:**
486
335
 
487
- **Sample Validation Callback Payload:**
488
- ```ruby
336
+ ```json
489
337
  {
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"
338
+ "ResponseCode": "0",
339
+ "ResponseDescription": "success"
503
340
  }
504
341
  ```
505
342
 
506
- **Sample Confirmation Callback Payload:**
507
- ```ruby
343
+ **Simulate immediate response:**
344
+
345
+ ```json
508
346
  {
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"
347
+ "ResponseCode": "0",
348
+ "ResponseDescription": "Accept the service request successfully."
522
349
  }
523
350
  ```
524
351
 
525
- **Validation Response**: Your validation endpoint should return:
526
- ```ruby
352
+ **Confirmation callback** (Safaricom `POST` to `confirmation_url` on real payments):
353
+
354
+ ```json
527
355
  {
528
- "ResultCode" => 0, # 0 = Accept, C2B00011 = Reject
529
- "ResultDesc" => "Accepted"
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"
530
369
  }
531
370
  ```
532
371
 
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.
372
+ ---
536
373
 
537
- **Using ENV variables:**
538
- ```ruby
539
- # Set ENV variables: initiator, security_credential, result_url, queue_timeout_url
540
- result = MpesaStk::AccountBalance.query({})
541
- ```
374
+ ### Transaction status
542
375
 
543
- **Using hash parameters:**
544
376
  ```ruby
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
- })
377
+ MpesaStk::TransactionStatus.call('RKTQDM7W6S')
554
378
  ```
555
379
 
556
- **Account Balance Callbacks:**
380
+ **Immediate response:**
557
381
 
558
- Account balance queries send results to `result_url` and `queue_timeout_url`.
559
-
560
- **Sample Result Callback Payload:**
561
- ```ruby
382
+ ```json
562
383
  {
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
- }
384
+ "ResponseCode": "0",
385
+ "ResponseDescription": "The service request is processed successfully.",
386
+ "ConversationID": "AG_20240101_12345678901234567890",
387
+ "OriginatorConversationID": "12345-67890-1"
576
388
  }
577
389
  ```
578
390
 
579
- ### Reversal
391
+ **Result callback** (`result_url`) includes transaction details inside `Result.ResultParameters` (same pattern as B2C).
580
392
 
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.
393
+ ---
582
394
 
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
- ```
395
+ ### Account balance
588
396
 
589
- **Using hash parameters:**
590
397
  ```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
- })
398
+ MpesaStk::AccountBalance.call
601
399
  ```
602
400
 
603
- **Reversal Callbacks:**
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
+ ```
604
411
 
605
- Reversal requests send results to `result_url` and `queue_timeout_url`.
412
+ **Result callback** (`result_url`) balance in `ResultParameters`:
606
413
 
607
- **Sample Result Callback Payload:**
608
- ```ruby
414
+ ```json
609
415
  {
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"}
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" }
624
426
  ]
625
427
  }
626
428
  }
627
429
  }
628
430
  ```
629
431
 
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.).
432
+ ---
633
433
 
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
- ```
434
+ ### Reversal
660
435
 
661
- **Using hash parameters:**
662
436
  ```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
- })
437
+ MpesaStk::Reversal.call('RKTQDM7W6S', 100)
693
438
  ```
694
439
 
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({})
440
+ **Immediate response:**
709
441
 
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"
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
+ }
733
449
  ```
734
450
 
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
- ```
451
+ Outcome is confirmed on `result_url` via the standard `Result` object.
750
452
 
751
- **Messaging Operations:**
453
+ ---
752
454
 
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.
455
+ ### Ratiba (standing orders)
754
456
 
755
- **Using ENV variables:**
756
457
  ```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
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'
773
465
  )
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
466
  ```
784
467
 
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
- ```
468
+ **Immediate response:**
800
469
 
801
- ### IMSI/SWAP Operations
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
+ ```
802
478
 
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.
479
+ Standing-order charge results are delivered to `callback_url` per Daraja Ratiba documentation.
804
480
 
805
- **Using ENV variables:**
806
- ```ruby
807
- # Check ATI (v1)
808
- result = MpesaStk::IMSI.check_ati("254712345678", {})
481
+ ---
809
482
 
810
- # Check ATI (v2)
811
- result = MpesaStk::IMSI.check_ati("254712345678", {}, version: "v2")
812
- ```
483
+ ### Pull transactions
813
484
 
814
- **Using hash parameters:**
815
485
  ```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")
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')
827
488
  ```
828
489
 
829
- ### Pull Transactions
490
+ **Register immediate response:**
830
491
 
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.
492
+ ```json
493
+ {
494
+ "ResponseCode": "0",
495
+ "ResponseDescription": "success"
496
+ }
497
+ ```
832
498
 
833
- **Register Pull URL:**
499
+ **Query immediate response**
834
500
 
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.
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:
836
502
 
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
- })
503
+ ```json
504
+ {
505
+ "ResponseCode": "0",
506
+ "data": []
507
+ }
844
508
  ```
845
509
 
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
- ```
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`.
857
511
 
858
- **Query Transactions:**
512
+ ---
859
513
 
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.
514
+ ### IMSI / SIM swap
861
515
 
862
- **Using ENV variables:**
863
516
  ```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
- )
517
+ MpesaStk::IMSI.call('254712345678', version: 'v2')
872
518
  ```
873
519
 
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",
879
- {
880
- "key" => "your_key",
881
- "secret" => "your_secret",
882
- "short_code" => "174379",
883
- "offset_value" => "0" # optional
884
- }
885
- )
886
- ```
520
+ **Immediate response**
887
521
 
888
- **Pull Transactions Callbacks:**
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).
889
523
 
890
- Pull transaction queries send results to the registered `callback_url`.
524
+ ---
891
525
 
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
- ```
526
+ ### IoT SIM portal
912
527
 
913
- ## Handling Callbacks in Your Application
528
+ Shortcuts:
914
529
 
915
- **Rails Example:**
916
530
  ```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
531
+ MpesaStk::IoT.list_sims(start_at_index: 0, page_size: 10)
532
+ MpesaStk::IoT.send_message('0110100606', 'Hello device')
973
533
  ```
974
534
 
975
- **Sinatra Example:**
535
+ Or dispatch any instance method by name:
536
+
976
537
  ```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
538
+ MpesaStk::IoT.call(:query_lifecycle_status, '0110100606')
539
+ MpesaStk::IoT.call(:search_messages, 'keyword', page_no: 1, page_size: 5)
986
540
  ```
987
541
 
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
542
+ **List SIMs / messages**
998
543
 
999
- ### Success Response Format
544
+ Tests stub a minimal success body (again, **not** a guarantee of empty inventory):
1000
545
 
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
1006
-
1007
- **Example Success Response:**
1008
- ```ruby
546
+ ```json
1009
547
  {
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"
548
+ "success": true,
549
+ "data": []
1015
550
  }
1016
551
  ```
1017
552
 
1018
- ### Error Response Format
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.
1019
554
 
1020
- Error responses include:
1021
- - `errorCode`: Error code (e.g., "500.001.1001")
1022
- - `errorMessage`: Error description
1023
- - `requestId`: Request identifier
555
+ **Send message** — tests only assert success; a plausible live shape might look like:
1024
556
 
1025
- **Example Error Response:**
1026
- ```ruby
557
+ ```json
1027
558
  {
1028
- "requestId" => "13022-8633727-1",
1029
- "errorCode" => "500.001.1001",
1030
- "errorMessage" => "Error Message"
559
+ "success": true,
560
+ "data": {
561
+ "messageId": "msg-12345",
562
+ "status": "sent"
563
+ }
1031
564
  }
1032
565
  ```
1033
566
 
1034
- ### Error Handling
567
+ That send-message example is **illustrative**; confirm against the IoT portal API docs for your environment.
1035
568
 
1036
- All methods raise `StandardError` with descriptive messages when:
1037
- - HTTP requests fail
1038
- - Required configuration is missing
1039
- - API returns error responses
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.
1040
570
 
1041
- ```ruby
1042
- begin
1043
- result = MpesaStk::PushPayment.call("500", "254712345678")
1044
- rescue StandardError => e
1045
- puts "Error: #{e.message}"
1046
- end
1047
- ```
571
+ ---
1048
572
 
1049
573
  ## Development
1050
574
 
1051
- After checking out the repo, run `bin/setup` to install dependencies. Then, run the tests:
1052
-
1053
575
  ```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
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
1059
582
  ```
1060
583
 
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+).
584
+ Use the [Safaricom sandbox](https://developer.safaricom.co.ke/) and valid test credentials in `.env` for manual checks.
1064
585
 
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).
586
+ ```bash
587
+ bundle exec rake test # 75 tests, WebMock (no live API calls)
588
+ bundle exec rubocop
589
+ ```
1066
590
 
1067
591
  ## Contributing
1068
592
 
1069
- Bug reports and pull requests are welcome on GitHub at https://github.com/mboya/mpesa_stk.
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.
1070
597
 
1071
- To Contribute to this gem,
1072
- * Comment on the issue you would like to work on solving.
1073
- * Mark the issue as in progress by adding an `in-progress` label.
1074
- * Fork the project to your github repository (This project only accepts PRs from forks)
1075
- * Submit the PR after the implementation all unfinished PRs for an issue should have a WIP indicated beside it
1076
- * Every PR should have a link to the issue being solved
1077
- * Checkout this [github best practices](https://github.com/skyscreamer/yoga/wiki/GitHub-Best-Practices) for more info.
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).
1078
599
 
1079
600
  ## License
1080
601
 
1081
- The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
1082
-
1083
- ## Code of Conduct
1084
-
1085
- 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).
1086
-
602
+ Copyright (c) 2018 mboya. Released under the [MIT License](LICENSE.txt).