mpesa_stk 1.2.1.1 → 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.
- checksums.yaml +4 -4
- data/.github/workflows/codeql-analysis.yml +70 -0
- data/.github/workflows/cop.yml +18 -14
- data/.rubocop.yml +62 -0
- data/.sample.env +23 -1
- data/CHANGELOG.md +244 -0
- data/Gemfile +4 -2
- data/Gemfile.lock +81 -38
- data/README.md +1001 -32
- data/Rakefile +8 -6
- data/SECURITY.md +21 -0
- data/bin/console +4 -3
- data/lib/mpesa_stk/access_token.rb +58 -26
- data/lib/mpesa_stk/account_balance.rb +142 -0
- data/lib/mpesa_stk/b2b.rb +153 -0
- data/lib/mpesa_stk/b2c.rb +151 -0
- data/lib/mpesa_stk/c2b.rb +127 -0
- data/lib/mpesa_stk/imsi.rb +70 -0
- data/lib/mpesa_stk/iot.rb +206 -0
- data/lib/mpesa_stk/pull_transactions.rb +126 -0
- data/lib/mpesa_stk/push.rb +160 -0
- data/lib/mpesa_stk/push_payment.rb +49 -21
- data/lib/mpesa_stk/ratiba.rb +118 -0
- data/lib/mpesa_stk/reversal.rb +147 -0
- data/lib/mpesa_stk/stk_push_query.rb +109 -0
- data/lib/mpesa_stk/transaction_status.rb +145 -0
- data/lib/mpesa_stk/version.rb +25 -1
- data/lib/mpesa_stk.rb +38 -1
- data/mpesa_stk.gemspec +33 -21
- metadata +117 -44
data/README.md
CHANGED
|
@@ -1,7 +1,33 @@
|
|
|
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
|
-
|
|
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.
|
|
28
|
+
|
|
29
|
+
[](https://badge.fury.io/rb/mpesa_stk.svg)
|
|
30
|
+

|
|
5
31
|
|
|
6
32
|
## Installation
|
|
7
33
|
|
|
@@ -18,28 +44,59 @@ gem install mpesa_stk
|
|
|
18
44
|
```
|
|
19
45
|
|
|
20
46
|
# Getting Started
|
|
21
|
-
|
|
22
|
-
|
|
47
|
+
|
|
48
|
+
## Prerequisites
|
|
49
|
+
|
|
50
|
+
**Redis:** This gem has a [Redis](https://redis.io/) dependency, so make sure it's running:
|
|
51
|
+
```bash
|
|
23
52
|
$ redis-server
|
|
24
53
|
```
|
|
25
54
|
|
|
55
|
+
You can verify Redis is running:
|
|
56
|
+
```bash
|
|
57
|
+
$ redis-cli ping
|
|
58
|
+
# Should return: PONG
|
|
59
|
+
```
|
|
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
|
|
65
|
+
```
|
|
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
|
+
|
|
26
69
|
you need to setup your environment variables, checkout `.sample.env` for the values you need.
|
|
27
70
|
or run
|
|
28
71
|
```ruby
|
|
29
72
|
$ cp .sample.env .env
|
|
30
73
|
```
|
|
31
|
-
open `.env` on your editor and add the missing
|
|
32
|
-
```
|
|
33
|
-
key=""
|
|
34
|
-
secret=""
|
|
35
|
-
business_short_code=""
|
|
36
|
-
business_passkey=""
|
|
37
|
-
callback_url=""
|
|
38
|
-
```
|
|
74
|
+
open `.env` on your editor and add the missing variables. See `.sample.env` for all available configuration options.
|
|
39
75
|
|
|
40
|
-
|
|
41
|
-
* `
|
|
42
|
-
* `
|
|
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
|
|
94
|
+
|
|
95
|
+
`Prod:`
|
|
96
|
+
|
|
97
|
+
when going live there information will be sent to your email.
|
|
98
|
+
|
|
99
|
+
for `buy_goods` push `business_short_code` will be equivalent to `store number` and `till_number` will remain as is.
|
|
43
100
|
|
|
44
101
|
### Testing out the gem in an actual Rails application
|
|
45
102
|
|
|
@@ -53,45 +110,957 @@ https://github.com/mboya/stk
|
|
|
53
110
|
#### Sample application
|
|
54
111
|
Check out a rails sample application [here](https://github.com/mboya/stk)
|
|
55
112
|
|
|
56
|
-
###
|
|
113
|
+
### Quick Start Examples
|
|
57
114
|
|
|
115
|
+
**Single App (using ENV variables):**
|
|
58
116
|
```ruby
|
|
59
117
|
$ irb
|
|
118
|
+
> require 'mpesa_stk'
|
|
119
|
+
> MpesaStk::PushPayment.call("500", "254711222333")
|
|
120
|
+
```
|
|
121
|
+
|
|
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
|
+
```
|
|
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
|
+

|
|
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({})
|
|
541
|
+
```
|
|
542
|
+
|
|
543
|
+
**Using hash parameters:**
|
|
544
|
+
```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
|
+
})
|
|
554
|
+
```
|
|
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:**
|
|
561
|
+
```ruby
|
|
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
|
+
}
|
|
577
|
+
```
|
|
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", {})
|
|
60
587
|
```
|
|
61
588
|
|
|
589
|
+
**Using hash parameters:**
|
|
62
590
|
```ruby
|
|
63
|
-
|
|
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
|
+
})
|
|
64
601
|
```
|
|
65
602
|
|
|
603
|
+
**Reversal Callbacks:**
|
|
604
|
+
|
|
605
|
+
Reversal requests send results to `result_url` and `queue_timeout_url`.
|
|
606
|
+
|
|
607
|
+
**Sample Result Callback Payload:**
|
|
66
608
|
```ruby
|
|
67
|
-
|
|
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
|
+
}
|
|
68
628
|
```
|
|
69
629
|
|
|
70
|
-
|
|
71
|
-
|
|
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",
|
|
72
879
|
{
|
|
73
|
-
"
|
|
74
|
-
"
|
|
75
|
-
"
|
|
76
|
-
"
|
|
77
|
-
"CustomerMessage"=>"Success. Request accepted for processing"
|
|
880
|
+
"key" => "your_key",
|
|
881
|
+
"secret" => "your_secret",
|
|
882
|
+
"short_code" => "174379",
|
|
883
|
+
"offset_value" => "0" # optional
|
|
78
884
|
}
|
|
885
|
+
)
|
|
79
886
|
```
|
|
80
887
|
|
|
81
|
-
|
|
888
|
+
**Pull Transactions Callbacks:**
|
|
82
889
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
890
|
+
Pull transaction queries send results to the registered `callback_url`.
|
|
891
|
+
|
|
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
|
+
```
|
|
86
912
|
|
|
87
|
-
|
|
913
|
+
## Handling Callbacks in Your Application
|
|
88
914
|
|
|
89
|
-
|
|
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
|
|
90
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
|
|
1006
|
+
|
|
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
|
|
1047
|
+
```
|
|
91
1048
|
|
|
92
1049
|
## Development
|
|
93
1050
|
|
|
94
|
-
After checking out the repo, run `bin/setup` to install dependencies. Then, run
|
|
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+).
|
|
95
1064
|
|
|
96
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).
|
|
97
1066
|
|