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.
- checksums.yaml +4 -4
- data/.github/workflows/codeql-analysis.yml +0 -0
- data/.github/workflows/cop.yml +0 -0
- data/.gitignore +2 -0
- data/.rubocop.yml +1 -0
- data/.sample.env +0 -0
- data/CHANGELOG.md +15 -0
- data/CODE_OF_CONDUCT.md +0 -0
- data/Gemfile +2 -0
- data/Gemfile.lock +43 -34
- data/LICENSE.txt +0 -0
- data/MIGRATION.md +77 -0
- data/README.md +371 -855
- data/Rakefile +0 -0
- data/SECURITY.md +0 -0
- data/bin/index.jpeg +0 -0
- data/lib/mpesa_stk/access_token.rb +4 -3
- data/lib/mpesa_stk/account_balance.rb +18 -130
- data/lib/mpesa_stk/b2b.rb +20 -129
- data/lib/mpesa_stk/b2c.rb +18 -130
- data/lib/mpesa_stk/c2b.rb +26 -109
- data/lib/mpesa_stk/client.rb +67 -0
- data/lib/mpesa_stk/config.rb +36 -0
- data/lib/mpesa_stk/imsi.rb +12 -54
- data/lib/mpesa_stk/iot.rb +35 -74
- data/lib/mpesa_stk/pull_transactions.rb +27 -109
- data/lib/mpesa_stk/push.rb +36 -127
- data/lib/mpesa_stk/ratiba.rb +19 -100
- data/lib/mpesa_stk/reversal.rb +23 -130
- data/lib/mpesa_stk/stk_push_query.rb +22 -93
- data/lib/mpesa_stk/transaction_status.rb +22 -129
- data/lib/mpesa_stk/version.rb +1 -23
- data/lib/mpesa_stk.rb +12 -24
- data/mpesa_stk.gemspec +5 -4
- metadata +27 -11
- data/lib/mpesa_stk/push_payment.rb +0 -99
data/README.md
CHANGED
|
@@ -1,219 +1,219 @@
|
|
|
1
1
|
# MpesaStk
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
|
|
5
|
+
[](https://badge.fury.io/rb/mpesa_stk)
|
|
6
|
+
[](https://github.com/mboya/mpesa_stk/actions/workflows/cop.yml)
|
|
6
7
|
|
|
7
|
-
|
|
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
|
-
|
|
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
|
-

|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
23
|
+
bundle install
|
|
53
24
|
```
|
|
54
25
|
|
|
55
|
-
|
|
26
|
+
Or install directly:
|
|
27
|
+
|
|
56
28
|
```bash
|
|
57
|
-
|
|
58
|
-
# Should return: PONG
|
|
29
|
+
gem install mpesa_stk
|
|
59
30
|
```
|
|
60
31
|
|
|
61
|
-
|
|
32
|
+
## Configuration
|
|
33
|
+
|
|
34
|
+
Copy the sample environment file and fill in your Daraja credentials:
|
|
35
|
+
|
|
62
36
|
```bash
|
|
63
|
-
|
|
64
|
-
$ bundle --version # Should show 2.4.0 or higher
|
|
37
|
+
cp .sample.env .env
|
|
65
38
|
```
|
|
66
39
|
|
|
67
|
-
|
|
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
|
-
|
|
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
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
|
|
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
|
-
**
|
|
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
|
-
|
|
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
|
-
`
|
|
72
|
+
STK Push (PayBill) — one line when `.env` is configured:
|
|
96
73
|
|
|
97
|
-
|
|
74
|
+
```ruby
|
|
75
|
+
require 'mpesa_stk'
|
|
98
76
|
|
|
99
|
-
|
|
77
|
+
response = MpesaStk::Push.call(100, '254712345678')
|
|
78
|
+
# => { "ResponseCode" => "0", "CheckoutRequestID" => "ws_CO_...", ... }
|
|
79
|
+
```
|
|
100
80
|
|
|
101
|
-
|
|
81
|
+
Register your `callback_url` and handle the STK result on your server ([STK Push](#stk-push)).
|
|
102
82
|
|
|
103
|
-
|
|
83
|
+
## Usage
|
|
104
84
|
|
|
105
|
-
|
|
85
|
+
Every client exposes a **single entry point**:
|
|
106
86
|
|
|
107
|
-
|
|
108
|
-
|
|
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
|
-
|
|
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
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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
|
-
|
|
111
|
+
## Understanding responses
|
|
139
112
|
|
|
140
|
-
|
|
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
|
-
|
|
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
|
-
|
|
121
|
+
On HTTP errors the gem raises `StandardError` with status and body, for example:
|
|
145
122
|
|
|
146
|
-
|
|
123
|
+
```json
|
|
124
|
+
{
|
|
125
|
+
"errorCode": "400.001.1001",
|
|
126
|
+
"errorMessage": "Bad Request"
|
|
127
|
+
}
|
|
128
|
+
```
|
|
147
129
|
|
|
148
|
-
|
|
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
|
|
132
|
+
## API reference
|
|
155
133
|
|
|
156
|
-
|
|
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
|
-
|
|
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
|
-
|
|
161
|
+
MpesaStk::Push.call(100, '254712345678')
|
|
163
162
|
```
|
|
164
163
|
|
|
165
|
-
**
|
|
164
|
+
**Buy Goods** — requires `till_number` in `.env` or as a keyword:
|
|
165
|
+
|
|
166
166
|
```ruby
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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
|
-
|
|
176
|
-
result = MpesaStk::Push.pay_bill("500", "254712345678", hash)
|
|
171
|
+
**Per-request overrides** (multi-app or custom credentials):
|
|
177
172
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
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
|
-
**
|
|
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
|
-
```
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
"
|
|
194
|
-
|
|
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
|
-
|
|
197
|
+
Persist `CheckoutRequestID` to match the callback and STK query.
|
|
199
198
|
|
|
200
|
-
|
|
199
|
+
**Callback** (Safaricom `POST` to `callback_url` — not returned by the gem):
|
|
201
200
|
|
|
202
|
-
|
|
203
|
-
|
|
201
|
+
Success:
|
|
202
|
+
|
|
203
|
+
```json
|
|
204
204
|
{
|
|
205
|
-
"Body"
|
|
206
|
-
"stkCallback"
|
|
207
|
-
"MerchantRequestID"
|
|
208
|
-
"CheckoutRequestID"
|
|
209
|
-
"ResultCode"
|
|
210
|
-
"ResultDesc"
|
|
211
|
-
"CallbackMetadata"
|
|
212
|
-
"Item"
|
|
213
|
-
{"Name"
|
|
214
|
-
{"Name"
|
|
215
|
-
{"Name"
|
|
216
|
-
{"Name"
|
|
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
|
-
|
|
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
|
-
|
|
251
|
-
|
|
252
|
-
**Sample Result Callback Payload:**
|
|
253
|
-
```ruby
|
|
226
|
+
```json
|
|
254
227
|
{
|
|
255
|
-
"
|
|
256
|
-
"
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
**
|
|
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
|
-
|
|
313
|
-
```ruby
|
|
249
|
+
```json
|
|
314
250
|
{
|
|
315
|
-
"
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
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
|
-
|
|
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
|
-
|
|
262
|
+
---
|
|
354
263
|
|
|
355
|
-
|
|
264
|
+
### B2C
|
|
356
265
|
|
|
357
|
-
**Using ENV variables:**
|
|
358
266
|
```ruby
|
|
359
|
-
|
|
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
|
-
**
|
|
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
|
-
|
|
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
|
-
|
|
281
|
+
**Callback** (`result_url` / `queue_timeout_url`):
|
|
403
282
|
|
|
404
|
-
|
|
405
|
-
```ruby
|
|
283
|
+
```json
|
|
406
284
|
{
|
|
407
|
-
"Result"
|
|
408
|
-
"ResultType"
|
|
409
|
-
"ResultCode"
|
|
410
|
-
"ResultDesc"
|
|
411
|
-
"OriginatorConversationID"
|
|
412
|
-
"ConversationID"
|
|
413
|
-
"TransactionID"
|
|
414
|
-
"ResultParameters"
|
|
415
|
-
"ResultParameter"
|
|
416
|
-
{"Key"
|
|
417
|
-
{"Key"
|
|
418
|
-
{"Key"
|
|
419
|
-
{"Key"
|
|
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
|
-
|
|
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
|
-
|
|
306
|
+
### B2B
|
|
435
307
|
|
|
436
|
-
**Using ENV variables:**
|
|
437
308
|
```ruby
|
|
438
|
-
|
|
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
|
-
**
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
"
|
|
450
|
-
"
|
|
451
|
-
"
|
|
452
|
-
"
|
|
453
|
-
|
|
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
|
-
|
|
323
|
+
Final payment outcome arrives on `result_url` in the same `Result` wrapper format as B2C.
|
|
458
324
|
|
|
459
|
-
|
|
325
|
+
---
|
|
460
326
|
|
|
461
|
-
|
|
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
|
-
|
|
473
|
-
|
|
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
|
-
**
|
|
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
|
-
|
|
488
|
-
```ruby
|
|
336
|
+
```json
|
|
489
337
|
{
|
|
490
|
-
"
|
|
491
|
-
"
|
|
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
|
-
**
|
|
507
|
-
|
|
343
|
+
**Simulate — immediate response:**
|
|
344
|
+
|
|
345
|
+
```json
|
|
508
346
|
{
|
|
509
|
-
"
|
|
510
|
-
"
|
|
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
|
-
**
|
|
526
|
-
|
|
352
|
+
**Confirmation callback** (Safaricom `POST` to `confirmation_url` on real payments):
|
|
353
|
+
|
|
354
|
+
```json
|
|
527
355
|
{
|
|
528
|
-
"
|
|
529
|
-
"
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
**
|
|
380
|
+
**Immediate response:**
|
|
557
381
|
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
**Sample Result Callback Payload:**
|
|
561
|
-
```ruby
|
|
382
|
+
```json
|
|
562
383
|
{
|
|
563
|
-
"
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
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
|
-
|
|
391
|
+
**Result callback** (`result_url`) includes transaction details inside `Result.ResultParameters` (same pattern as B2C).
|
|
580
392
|
|
|
581
|
-
|
|
393
|
+
---
|
|
582
394
|
|
|
583
|
-
|
|
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
|
-
|
|
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
|
-
**
|
|
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
|
-
|
|
412
|
+
**Result callback** (`result_url`) — balance in `ResultParameters`:
|
|
606
413
|
|
|
607
|
-
|
|
608
|
-
```ruby
|
|
414
|
+
```json
|
|
609
415
|
{
|
|
610
|
-
"Result"
|
|
611
|
-
"ResultType"
|
|
612
|
-
"ResultCode"
|
|
613
|
-
"ResultDesc"
|
|
614
|
-
"OriginatorConversationID"
|
|
615
|
-
"ConversationID"
|
|
616
|
-
"
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
{"Key"
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
**
|
|
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
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
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
|
-
|
|
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
|
-
|
|
453
|
+
---
|
|
752
454
|
|
|
753
|
-
|
|
455
|
+
### Ratiba (standing orders)
|
|
754
456
|
|
|
755
|
-
**Using ENV variables:**
|
|
756
457
|
```ruby
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
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
|
-
**
|
|
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
|
-
|
|
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
|
-
|
|
479
|
+
Standing-order charge results are delivered to `callback_url` per Daraja Ratiba documentation.
|
|
804
480
|
|
|
805
|
-
|
|
806
|
-
```ruby
|
|
807
|
-
# Check ATI (v1)
|
|
808
|
-
result = MpesaStk::IMSI.check_ati("254712345678", {})
|
|
481
|
+
---
|
|
809
482
|
|
|
810
|
-
|
|
811
|
-
result = MpesaStk::IMSI.check_ati("254712345678", {}, version: "v2")
|
|
812
|
-
```
|
|
483
|
+
### Pull transactions
|
|
813
484
|
|
|
814
|
-
**Using hash parameters:**
|
|
815
485
|
```ruby
|
|
816
|
-
|
|
817
|
-
|
|
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
|
-
|
|
490
|
+
**Register — immediate response:**
|
|
830
491
|
|
|
831
|
-
|
|
492
|
+
```json
|
|
493
|
+
{
|
|
494
|
+
"ResponseCode": "0",
|
|
495
|
+
"ResponseDescription": "success"
|
|
496
|
+
}
|
|
497
|
+
```
|
|
832
498
|
|
|
833
|
-
**
|
|
499
|
+
**Query — immediate response**
|
|
834
500
|
|
|
835
|
-
|
|
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
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
"nominated_number" => "254712345678"
|
|
843
|
-
})
|
|
503
|
+
```json
|
|
504
|
+
{
|
|
505
|
+
"ResponseCode": "0",
|
|
506
|
+
"data": []
|
|
507
|
+
}
|
|
844
508
|
```
|
|
845
509
|
|
|
846
|
-
**
|
|
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
|
-
|
|
512
|
+
---
|
|
859
513
|
|
|
860
|
-
|
|
514
|
+
### IMSI / SIM swap
|
|
861
515
|
|
|
862
|
-
**Using ENV variables:**
|
|
863
516
|
```ruby
|
|
864
|
-
|
|
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
|
-
**
|
|
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
|
-
**
|
|
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
|
-
|
|
524
|
+
---
|
|
891
525
|
|
|
892
|
-
|
|
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
|
-
|
|
528
|
+
Shortcuts:
|
|
914
529
|
|
|
915
|
-
**Rails Example:**
|
|
916
530
|
```ruby
|
|
917
|
-
|
|
918
|
-
|
|
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
|
-
|
|
535
|
+
Or dispatch any instance method by name:
|
|
536
|
+
|
|
976
537
|
```ruby
|
|
977
|
-
|
|
978
|
-
|
|
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
|
-
**
|
|
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
|
-
|
|
544
|
+
Tests stub a minimal success body (again, **not** a guarantee of empty inventory):
|
|
1000
545
|
|
|
1001
|
-
|
|
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
|
-
"
|
|
1011
|
-
"
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1026
|
-
```ruby
|
|
557
|
+
```json
|
|
1027
558
|
{
|
|
1028
|
-
"
|
|
1029
|
-
"
|
|
1030
|
-
|
|
559
|
+
"success": true,
|
|
560
|
+
"data": {
|
|
561
|
+
"messageId": "msg-12345",
|
|
562
|
+
"status": "sent"
|
|
563
|
+
}
|
|
1031
564
|
}
|
|
1032
565
|
```
|
|
1033
566
|
|
|
1034
|
-
|
|
567
|
+
That send-message example is **illustrative**; confirm against the IoT portal API docs for your environment.
|
|
1035
568
|
|
|
1036
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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).
|