kwtsms 0.3.1 → 0.4.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/CHANGELOG.md +19 -0
- data/LICENSE +1 -1
- data/README.md +42 -30
- data/lib/kwtsms/client.rb +30 -16
- data/lib/kwtsms/errors.rb +5 -3
- data/lib/kwtsms/request.rb +13 -1
- data/lib/kwtsms/version.rb +1 -1
- data/lib/kwtsms.rb +5 -3
- metadata +10 -10
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8d0309851d3d99cd00dc43e57fcc1a6a3c325573a3ef5f03cc605b6155e2f8c5
|
|
4
|
+
data.tar.gz: ded82b65342f4b32e9aa42ced78bcdeeeb496efb2865b543ec4dfef1d8e8bc46
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: abf1aab66a4eef8ce7254f8a7963992b65a1dd70b527b893181f7b72a29f836aca7a32f76a01fe5507bc557fc8a82c4c9244b10ad9d4c0cee8a69b30327e7075
|
|
7
|
+
data.tar.gz: 5551f9ce6129a20e7f2c33c23395acf89910db177362f097f048618621624365c4b9a3add2efd8a82359f5af0f40597230dc48d3d1ca6ef96d8a17241ef8ef1b
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,25 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
## [0.4.0] - 2026-11-07
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- The client sends a User-Agent `kwtsms-ruby/<version>` (name and version only, no personal data) so kwtSMS can see which integrations are in use. An optional `app_name:` keyword on `KwtSMS::Client.new` puts your app's token in front, e.g. `kwtsms-wordpress/3.5.2 kwtsms-ruby/0.3.1`. New `KwtSMS.build_user_agent` and `client.user_agent`.
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
- Package author and license holder are now kwtSMS; contact email support@kwtsms.com.
|
|
17
|
+
|
|
18
|
+
- `send_with_retry`: ERR028 retry is now off by default (`max_retries: 0`), and the wait is a new `wait_seconds:` keyword (default 10) instead of a hardcoded 16 seconds. Duplicate SMS protection (ERR028) is opt-in per account and its window is configurable per account (10 seconds by default); when on, the whole request is rejected and nothing is charged.
|
|
19
|
+
- ERR028 action text no longer states a fixed 15 seconds.
|
|
20
|
+
- ERR006 and ERR025 action text, docs and examples use the placeholder `965XXXXXXXX` instead of a sample mobile number; tests read the phone number from `KWTSMS_TEST_NUMBER` and skip the tests that need it when it is unset.
|
|
21
|
+
- The live 250-number bulk test is replaced by an offline mocked test of the 200 + 50 batching; live tests send only to `KWTSMS_TEST_NUMBER`.
|
|
22
|
+
|
|
23
|
+
### Docs
|
|
24
|
+
|
|
25
|
+
- Test mode: test messages are queued but not delivered. Their credits are held until you delete them from the Sending Queue in your kwtsms.com account.
|
|
26
|
+
|
|
8
27
|
## [0.3.1] - 2026-03-15
|
|
9
28
|
|
|
10
29
|
### Fixed
|
data/LICENSE
CHANGED
data/README.md
CHANGED
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://rubygems.org/gems/kwtsms)
|
|
4
4
|
[](https://rubygems.org/gems/kwtsms)
|
|
5
|
-
[](https://github.com/kwtsms/kwtsms-ruby/actions/workflows/ci.yml)
|
|
6
|
+
[](https://github.com/kwtsms/kwtsms-ruby/actions/workflows/codeql.yml)
|
|
7
|
+
[](https://github.com/kwtsms/kwtsms-ruby/actions/workflows/audit.yml)
|
|
8
|
+
[](https://github.com/kwtsms/kwtsms-ruby/actions/workflows/gitguardian.yml)
|
|
9
|
+
[](https://scorecard.dev/viewer/?uri=github.com/kwtsms/kwtsms-ruby)
|
|
10
10
|
[](https://www.ruby-lang.org/)
|
|
11
11
|
[](LICENSE)
|
|
12
12
|
|
|
@@ -52,7 +52,7 @@ ok, balance, err = sms.verify
|
|
|
52
52
|
puts "Balance: #{balance}" if ok
|
|
53
53
|
|
|
54
54
|
# Send SMS
|
|
55
|
-
result = sms.send_sms("
|
|
55
|
+
result = sms.send_sms("965XXXXXXXX", "Your OTP for MyApp is: 123456")
|
|
56
56
|
puts "msg-id: #{result['msg-id']}" if result["result"] == "OK"
|
|
57
57
|
```
|
|
58
58
|
|
|
@@ -82,6 +82,16 @@ sms = KwtSMS::Client.new(
|
|
|
82
82
|
)
|
|
83
83
|
```
|
|
84
84
|
|
|
85
|
+
### User-Agent
|
|
86
|
+
|
|
87
|
+
Every API request carries the User-Agent `kwtsms-ruby/<version>`: the library name and version only, no
|
|
88
|
+
personal or customer data. Pass your own product token as `app_name:` to put it in front:
|
|
89
|
+
|
|
90
|
+
```ruby
|
|
91
|
+
sms = KwtSMS::Client.new("ruby_username", "ruby_password", app_name: "my-shop/2.1.0")
|
|
92
|
+
# User-Agent: my-shop/2.1.0 kwtsms-ruby/0.3.1
|
|
93
|
+
```
|
|
94
|
+
|
|
85
95
|
## API Reference
|
|
86
96
|
|
|
87
97
|
### verify
|
|
@@ -109,13 +119,13 @@ Send SMS to one or more numbers.
|
|
|
109
119
|
|
|
110
120
|
```ruby
|
|
111
121
|
# Single number
|
|
112
|
-
result = sms.send_sms("
|
|
122
|
+
result = sms.send_sms("965XXXXXXXX", "Hello!")
|
|
113
123
|
|
|
114
124
|
# Multiple numbers
|
|
115
|
-
result = sms.send_sms(["
|
|
125
|
+
result = sms.send_sms(["965XXXXXXXX", "96512345678"], "Bulk message")
|
|
116
126
|
|
|
117
127
|
# Override sender ID
|
|
118
|
-
result = sms.send_sms("
|
|
128
|
+
result = sms.send_sms("965XXXXXXXX", "Hello!", sender: "MY-APP")
|
|
119
129
|
```
|
|
120
130
|
|
|
121
131
|
Response on success:
|
|
@@ -133,10 +143,10 @@ Response on success:
|
|
|
133
143
|
|
|
134
144
|
### send_with_retry
|
|
135
145
|
|
|
136
|
-
Send with
|
|
146
|
+
Send with optional retry on ERR028. ERR028 comes from Duplicate SMS protection, which is opt-in per account (in the account's API settings). When it is on and any number in the request was sent the same message within the account's window (10 seconds by default, configurable per account), the whole request is rejected with ERR028 and nothing is charged. Retry is off by default (`max_retries: 0`); set `wait_seconds:` to your account's window.
|
|
137
147
|
|
|
138
148
|
```ruby
|
|
139
|
-
result = sms.send_with_retry("
|
|
149
|
+
result = sms.send_with_retry("965XXXXXXXX", "Hello!", max_retries: 1, wait_seconds: 10)
|
|
140
150
|
```
|
|
141
151
|
|
|
142
152
|
### senderids
|
|
@@ -161,7 +171,7 @@ result = sms.coverage
|
|
|
161
171
|
Validate phone numbers.
|
|
162
172
|
|
|
163
173
|
```ruby
|
|
164
|
-
result = sms.validate(["
|
|
174
|
+
result = sms.validate(["965XXXXXXXX", "invalid", "+96512345678"])
|
|
165
175
|
puts result["ok"] # valid numbers
|
|
166
176
|
puts result["er"] # error numbers
|
|
167
177
|
puts result["rejected"] # locally rejected with error messages
|
|
@@ -171,7 +181,7 @@ puts result["rejected"] # locally rejected with error messages
|
|
|
171
181
|
|
|
172
182
|
```ruby
|
|
173
183
|
# Normalize phone number: Arabic digits, strip non-digits, strip leading zeros
|
|
174
|
-
KwtSMS.normalize_phone("+965
|
|
184
|
+
KwtSMS.normalize_phone("+965 XXXX XXXX") # => "965XXXXXXXX"
|
|
175
185
|
|
|
176
186
|
# Validate phone input (returns [valid, error, normalized])
|
|
177
187
|
valid, error, normalized = KwtSMS.validate_phone_input("user@email.com")
|
|
@@ -212,22 +222,24 @@ All formats are accepted and normalized automatically:
|
|
|
212
222
|
|
|
213
223
|
| Input | Normalized | Valid? |
|
|
214
224
|
|-------|-----------|--------|
|
|
215
|
-
| `
|
|
216
|
-
| `+
|
|
217
|
-
| `
|
|
218
|
-
| `965
|
|
219
|
-
| `965-
|
|
220
|
-
| `(965)
|
|
221
|
-
|
|
|
222
|
-
|
|
|
223
|
-
|
|
|
224
|
-
|
|
|
225
|
-
| `٩٦٥
|
|
226
|
-
|
|
|
227
|
-
| `965
|
|
225
|
+
| `965XXXXXXXX` | `965XXXXXXXX` | Yes |
|
|
226
|
+
| `+965XXXXXXXX` | `965XXXXXXXX` | Yes |
|
|
227
|
+
| `00965XXXXXXXX` | `965XXXXXXXX` | Yes |
|
|
228
|
+
| `965 XXXX XXXX` | `965XXXXXXXX` | Yes |
|
|
229
|
+
| `965-XXXX-XXXX` | `965XXXXXXXX` | Yes |
|
|
230
|
+
| `(965) XXXXXXXX` | `965XXXXXXXX` | Yes |
|
|
231
|
+
| `٩٦٥XXXXXXXX` | `965XXXXXXXX` | Yes |
|
|
232
|
+
| `۹۶۵XXXXXXXX` | `965XXXXXXXX` | Yes |
|
|
233
|
+
| `+٩٦٥XXXXXXXX` | `965XXXXXXXX` | Yes |
|
|
234
|
+
| `٠٠٩٦٥XXXXXXXX` | `965XXXXXXXX` | Yes |
|
|
235
|
+
| `٩٦٥ XXXX XXXX` | `965XXXXXXXX` | Yes |
|
|
236
|
+
| `٩٦٥-XXXX-XXXX` | `965XXXXXXXX` | Yes |
|
|
237
|
+
| `965` + 8 Arabic-Indic digits | `965XXXXXXXX` | Yes |
|
|
228
238
|
| `123456` (too short) | rejected | No |
|
|
229
239
|
| `user@gmail.com` | rejected | No |
|
|
230
240
|
|
|
241
|
+
`X` stands for any digit, in Latin, Arabic-Indic or Persian digits.
|
|
242
|
+
|
|
231
243
|
Normalization rules:
|
|
232
244
|
- Arabic-Indic and Extended Arabic-Indic digits converted to Latin
|
|
233
245
|
- Non-digit characters stripped (`+`, spaces, dashes, dots, brackets)
|
|
@@ -247,7 +259,7 @@ Messages are cleaned automatically before sending to prevent silent delivery fai
|
|
|
247
259
|
|
|
248
260
|
## CLI
|
|
249
261
|
|
|
250
|
-
For a standalone CLI tool that works on all operating systems, see [kwtsms-cli](https://github.com/
|
|
262
|
+
For a standalone CLI tool that works on all operating systems, see [kwtsms-cli](https://github.com/kwtsms/kwtsms-cli).
|
|
251
263
|
|
|
252
264
|
## Credential Management
|
|
253
265
|
|
|
@@ -331,7 +343,7 @@ Before going live:
|
|
|
331
343
|
Every API call is logged to a JSONL file (default: `kwtsms.log`):
|
|
332
344
|
|
|
333
345
|
```json
|
|
334
|
-
{"ts":"2026-03-06T12:00:00Z","endpoint":"send","request":{"username":"ruby_username","password":"***","mobile":"
|
|
346
|
+
{"ts":"2026-03-06T12:00:00Z","endpoint":"send","request":{"username":"ruby_username","password":"***","mobile":"965XXXXXXXX","message":"Hello"},"response":{"result":"OK","msg-id":"12345"},"ok":true,"error":null}
|
|
335
347
|
```
|
|
336
348
|
|
|
337
349
|
Passwords are always masked as `***`. Logging never crashes the main flow.
|
|
@@ -381,7 +393,7 @@ Check the **Sending Queue** at [kwtsms.com](https://www.kwtsms.com/login/). If y
|
|
|
381
393
|
|
|
382
394
|
**2. What is the difference between Test mode and Live mode?**
|
|
383
395
|
|
|
384
|
-
**Test mode** (`KWTSMS_TEST_MODE=1`) sends your message to the kwtSMS queue but does NOT deliver it to the handset.
|
|
396
|
+
**Test mode** (`KWTSMS_TEST_MODE=1`) sends your message to the kwtSMS queue but does NOT deliver it to the handset. Its credits are held until you delete it from the Sending Queue in your kwtsms.com account. Use this during development. **Live mode** (`KWTSMS_TEST_MODE=0`) delivers the message for real and deducts credits. Always develop in test mode and switch to live only when ready for production.
|
|
385
397
|
|
|
386
398
|
**3. What is a Sender ID and why should I not use "KWT-SMS" in production?**
|
|
387
399
|
|
|
@@ -407,7 +419,7 @@ International sending is **disabled by default** on kwtSMS accounts. Log in to y
|
|
|
407
419
|
- **[kwtSMS Dashboard](https://www.kwtsms.com/login/)**: Recharge credits, buy Sender IDs, view message logs, manage coverage
|
|
408
420
|
- **[Other Integrations](https://www.kwtsms.com/integrations.html)**: Plugins and integrations for other platforms and languages
|
|
409
421
|
- **[RubyGems](https://rubygems.org/gems/kwtsms)**: Package on RubyGems.org
|
|
410
|
-
- **[GitHub](https://github.com/
|
|
422
|
+
- **[GitHub](https://github.com/kwtsms/kwtsms-ruby)**: Source code and issue tracker
|
|
411
423
|
- **[Changelog](CHANGELOG.md)** | **[Contributing](CONTRIBUTING.md)** | **[Security](SECURITY.md)**
|
|
412
424
|
|
|
413
425
|
## License
|
data/lib/kwtsms/client.rb
CHANGED
|
@@ -19,21 +19,27 @@ module KwtSMS
|
|
|
19
19
|
# Quick start:
|
|
20
20
|
# sms = KwtSMS::Client.from_env
|
|
21
21
|
# ok, balance, err = sms.verify
|
|
22
|
-
# result = sms.send_sms("
|
|
23
|
-
# result = sms.send_sms("
|
|
22
|
+
# result = sms.send_sms("965XXXXXXXX", "Your OTP for MYAPP is: 123456")
|
|
23
|
+
# result = sms.send_sms("965XXXXXXXX", "Hello", sender: "OTHER-ID")
|
|
24
|
+
# 2026-11-05 08:07 +03: placeholder instead of a sample mobile number (it belongs to a real person), int-testnumber.
|
|
24
25
|
# balance = sms.balance
|
|
25
26
|
class Client
|
|
27
|
+
# 2026-11-05 14:19 +03: user_agent reader added, the User-Agent sent on every call (#939).
|
|
26
28
|
attr_reader :username, :sender_id, :test_mode, :log_file,
|
|
27
|
-
:cached_balance, :cached_purchased
|
|
29
|
+
:cached_balance, :cached_purchased, :user_agent
|
|
28
30
|
|
|
29
31
|
# @param username [String] API username (not your account phone number)
|
|
30
32
|
# @param password [String] API password
|
|
31
33
|
# @param sender_id [String] Sender ID shown on recipient's phone (default: "KWT-SMS")
|
|
32
34
|
# @param test_mode [Boolean] When true, messages are queued but not delivered (default: false)
|
|
33
35
|
# @param log_file [String] Path to JSONL log file. Set to "" to disable logging.
|
|
34
|
-
|
|
36
|
+
# @param app_name [String] Optional product token of the calling app or plugin, e.g. "kwtsms-wordpress/3.5.2",
|
|
37
|
+
# sent before the library token in the User-Agent (2026-11-05 14:19 +03, #939).
|
|
38
|
+
def initialize(username, password, sender_id: "KWT-SMS", test_mode: false, log_file: "kwtsms.log", app_name: "")
|
|
35
39
|
raise ArgumentError, "username and password are required" if username.nil? || username.empty? || password.nil? || password.empty?
|
|
36
40
|
|
|
41
|
+
# 2026-11-05 14:19 +03: identify the caller to the API (name and version only, no personal data), #939.
|
|
42
|
+
@user_agent = KwtSMS.build_user_agent(app_name)
|
|
37
43
|
@username = username
|
|
38
44
|
@password = password
|
|
39
45
|
@sender_id = sender_id
|
|
@@ -93,7 +99,7 @@ module KwtSMS
|
|
|
93
99
|
# balance: Float or nil
|
|
94
100
|
# error: nil or error message string
|
|
95
101
|
def verify
|
|
96
|
-
data = KwtSMS.api_request("balance", creds, @log_file)
|
|
102
|
+
data = KwtSMS.api_request("balance", creds, @log_file, @user_agent) # 2026-11-05 14:19 +03: UA (#939)
|
|
97
103
|
if data["result"] == "OK"
|
|
98
104
|
@cached_balance = data["available"].to_f
|
|
99
105
|
@cached_purchased = data["purchased"].to_f
|
|
@@ -119,7 +125,7 @@ module KwtSMS
|
|
|
119
125
|
# List sender IDs registered on this account via /senderid/.
|
|
120
126
|
# Returns a consistent hash. Never raises, never crashes.
|
|
121
127
|
def senderids
|
|
122
|
-
data = KwtSMS.api_request("senderid", creds, @log_file)
|
|
128
|
+
data = KwtSMS.api_request("senderid", creds, @log_file, @user_agent) # 2026-11-05 14:19 +03: UA (#939)
|
|
123
129
|
if data["result"] == "OK"
|
|
124
130
|
{ "result" => "OK", "senderids" => data["senderid"] || [] }
|
|
125
131
|
else
|
|
@@ -133,7 +139,7 @@ module KwtSMS
|
|
|
133
139
|
# List active country prefixes via /coverage/.
|
|
134
140
|
# Returns the full API response hash with error enrichment.
|
|
135
141
|
def coverage
|
|
136
|
-
data = KwtSMS.api_request("coverage", creds, @log_file)
|
|
142
|
+
data = KwtSMS.api_request("coverage", creds, @log_file, @user_agent) # 2026-11-05 14:19 +03: UA (#939)
|
|
137
143
|
KwtSMS.enrich_error(data)
|
|
138
144
|
rescue RuntimeError => e
|
|
139
145
|
{ "result" => "ERROR", "code" => "NETWORK", "description" => e.message,
|
|
@@ -180,7 +186,7 @@ module KwtSMS
|
|
|
180
186
|
|
|
181
187
|
payload = creds.merge("mobile" => valid_normalized.join(","))
|
|
182
188
|
begin
|
|
183
|
-
data = KwtSMS.api_request("validate", payload, @log_file)
|
|
189
|
+
data = KwtSMS.api_request("validate", payload, @log_file, @user_agent) # 2026-11-05 14:19 +03: UA (#939)
|
|
184
190
|
if data["result"] == "OK"
|
|
185
191
|
mobile = data["mobile"] || {}
|
|
186
192
|
result["ok"] = mobile["OK"] || []
|
|
@@ -262,7 +268,7 @@ module KwtSMS
|
|
|
262
268
|
"test" => @test_mode ? "1" : "0"
|
|
263
269
|
)
|
|
264
270
|
begin
|
|
265
|
-
result = KwtSMS.api_request("send", payload, @log_file)
|
|
271
|
+
result = KwtSMS.api_request("send", payload, @log_file, @user_agent) # 2026-11-05 14:19 +03: UA (#939)
|
|
266
272
|
rescue RuntimeError => e
|
|
267
273
|
return {
|
|
268
274
|
"result" => "ERROR",
|
|
@@ -282,21 +288,29 @@ module KwtSMS
|
|
|
282
288
|
result
|
|
283
289
|
end
|
|
284
290
|
|
|
285
|
-
# Send SMS, retrying
|
|
291
|
+
# Send SMS, optionally retrying on ERR028 (duplicate SMS protection).
|
|
286
292
|
#
|
|
287
|
-
#
|
|
288
|
-
#
|
|
293
|
+
# ERR028 is opt-in per account (Duplicate SMS protection in the account's
|
|
294
|
+
# API settings). When it is on and any number in the request was sent the
|
|
295
|
+
# same message within the account's window (10 seconds by default,
|
|
296
|
+
# configurable per account), the whole request is rejected and nothing is
|
|
297
|
+
# charged. Retry is off by default: ERR028 is returned like any other error
|
|
298
|
+
# unless max_retries > 0. All other errors are returned immediately.
|
|
299
|
+
#
|
|
300
|
+
# 2026-10-22 15:34 +03: retry off by default and wait is a parameter (default 10),
|
|
301
|
+
# was a hardcoded 3 retries x 16 s. The ERR028 window is per account, not a fixed 15 s.
|
|
289
302
|
#
|
|
290
303
|
# @param mobile [String, Array<String>] Phone number(s)
|
|
291
304
|
# @param message [String] Message text
|
|
292
305
|
# @param sender [String, nil] Sender ID override
|
|
293
|
-
# @param max_retries [Integer] Max ERR028 retries after first attempt (default
|
|
306
|
+
# @param max_retries [Integer] Max ERR028 retries after first attempt (default 0, no retry)
|
|
307
|
+
# @param wait_seconds [Numeric] Seconds to wait before each retry (default 10)
|
|
294
308
|
# @return [Hash] Same shape as send_sms. Never raises.
|
|
295
|
-
def send_with_retry(mobile, message, sender: nil, max_retries:
|
|
309
|
+
def send_with_retry(mobile, message, sender: nil, max_retries: 0, wait_seconds: 10)
|
|
296
310
|
result = send_sms(mobile, message, sender: sender)
|
|
297
311
|
retries = 0
|
|
298
312
|
while result["code"] == "ERR028" && retries < max_retries
|
|
299
|
-
sleep(
|
|
313
|
+
sleep(wait_seconds)
|
|
300
314
|
result = send_sms(mobile, message, sender: sender)
|
|
301
315
|
retries += 1
|
|
302
316
|
end
|
|
@@ -337,7 +351,7 @@ module KwtSMS
|
|
|
337
351
|
waits.each_with_index do |wait, attempt|
|
|
338
352
|
sleep(wait) if wait > 0
|
|
339
353
|
begin
|
|
340
|
-
data = KwtSMS.api_request("send", payload, @log_file)
|
|
354
|
+
data = KwtSMS.api_request("send", payload, @log_file, @user_agent) # 2026-11-05 14:19 +03: UA (#939)
|
|
341
355
|
rescue RuntimeError => e
|
|
342
356
|
errors << { "batch" => i + 1, "code" => "NETWORK", "description" => e.message }
|
|
343
357
|
data = nil
|
data/lib/kwtsms/errors.rb
CHANGED
|
@@ -9,7 +9,8 @@ module KwtSMS
|
|
|
9
9
|
"ERR003" => "Wrong API username or password. Check KWTSMS_USERNAME and KWTSMS_PASSWORD. These are your API credentials, not your account mobile number.",
|
|
10
10
|
"ERR004" => "This account does not have API access. Contact kwtSMS support to enable it.",
|
|
11
11
|
"ERR005" => "This account is blocked. Contact kwtSMS support.",
|
|
12
|
-
|
|
12
|
+
# 2026-11-05 08:07 +03: placeholder instead of a sample mobile number (it belongs to a real person), int-testnumber.
|
|
13
|
+
"ERR006" => "No valid phone numbers. Make sure each number includes the country code (e.g., 965XXXXXXXX for Kuwait, not XXXXXXXX).",
|
|
13
14
|
"ERR007" => "Too many numbers in a single request (maximum 200). Split into smaller batches.",
|
|
14
15
|
"ERR008" => "This sender ID is banned or not found. Sender IDs are case sensitive (\"Kuwait\" is not the same as \"KUWAIT\"). Check your registered sender IDs at kwtsms.com.",
|
|
15
16
|
"ERR009" => "Message is empty. Provide a non-empty message text.",
|
|
@@ -23,10 +24,11 @@ module KwtSMS
|
|
|
23
24
|
"ERR022" => "Delivery reports are not ready yet. Try again after 24 hours.",
|
|
24
25
|
"ERR023" => "Unknown delivery report error. Contact kwtSMS support.",
|
|
25
26
|
"ERR024" => "Your IP address is not in the API whitelist. Add it at kwtsms.com > Account > API > IP Lockdown, or disable IP lockdown.",
|
|
26
|
-
"ERR025" => "Invalid phone number. Make sure the number includes the country code (e.g.,
|
|
27
|
+
"ERR025" => "Invalid phone number. Make sure the number includes the country code (e.g., 965XXXXXXXX for Kuwait, not XXXXXXXX).",
|
|
27
28
|
"ERR026" => "This country is not activated on your account. Contact kwtSMS support to enable the destination country.",
|
|
28
29
|
"ERR027" => "HTML tags are not allowed in the message. Remove any HTML content and try again.",
|
|
29
|
-
|
|
30
|
+
# 2026-10-22 15:34 +03: ERR028 text corrected, the window is opt-in and per account (10 s default), never a fixed 15 s.
|
|
31
|
+
"ERR028" => "Sent to the same number too soon. Duplicate SMS protection is on for this account, so the whole request was rejected and nothing was charged. Wait for the account's window (10 seconds by default) before resending.",
|
|
30
32
|
"ERR029" => "Message ID does not exist or is incorrect.",
|
|
31
33
|
"ERR030" => "Message is stuck in the send queue with an error. Delete it at kwtsms.com > Queue to recover credits.",
|
|
32
34
|
"ERR031" => "Message rejected: bad language detected.",
|
data/lib/kwtsms/request.rb
CHANGED
|
@@ -12,13 +12,24 @@ module KwtSMS
|
|
|
12
12
|
# unix-timestamp values in API responses are server time, not UTC.
|
|
13
13
|
SERVER_TIMEZONE = "Asia/Kuwait (GMT+3)"
|
|
14
14
|
|
|
15
|
+
# Build the User-Agent "[app token ]kwtsms-ruby/<VERSION>". The app token is caller input: anything
|
|
16
|
+
# outside printable ASCII is removed (CR/LF would split the header), whitespace runs become "-",
|
|
17
|
+
# leading/trailing "-" are trimmed, and it is cut to 64 characters. Empty means the library token only.
|
|
18
|
+
# 2026-11-05 14:19 +03: added so kwtSMS can count accounts per integration (#939).
|
|
19
|
+
def self.build_user_agent(app_name = "")
|
|
20
|
+
app = app_name.to_s.gsub(/[^\x20-\x7e]+/, "").strip.gsub(/\s+/, "-").gsub(/\A-+|-+\z/, "")[0, 64]
|
|
21
|
+
lib = "kwtsms-ruby/#{VERSION}"
|
|
22
|
+
app.empty? ? lib : "#{app} #{lib}"
|
|
23
|
+
end
|
|
24
|
+
|
|
15
25
|
# POST to a kwtSMS REST/JSON API endpoint.
|
|
16
26
|
#
|
|
17
27
|
# Always sets Content-Type and Accept: application/json.
|
|
18
28
|
# Strips password from log entry.
|
|
19
29
|
# Returns parsed JSON hash.
|
|
20
30
|
# Raises RuntimeError on network / HTTP / parse failure.
|
|
21
|
-
|
|
31
|
+
# 2026-11-05 14:19 +03: user_agent parameter added, sent on every call (#939).
|
|
32
|
+
def self.api_request(endpoint, payload, log_file = "", user_agent = nil)
|
|
22
33
|
url = URI.parse("#{BASE_URL}#{endpoint}/")
|
|
23
34
|
|
|
24
35
|
safe_payload = payload.transform_keys(&:to_s).each_with_object({}) do |(k, v), h|
|
|
@@ -44,6 +55,7 @@ module KwtSMS
|
|
|
44
55
|
request = Net::HTTP::Post.new(url.path)
|
|
45
56
|
request["Content-Type"] = "application/json"
|
|
46
57
|
request["Accept"] = "application/json"
|
|
58
|
+
request["User-Agent"] = user_agent || build_user_agent # 2026-11-05 14:19 +03: kwtsms-ruby/<version> (#939)
|
|
47
59
|
request.body = JSON.generate(payload)
|
|
48
60
|
|
|
49
61
|
response = http.request(request)
|
data/lib/kwtsms/version.rb
CHANGED
data/lib/kwtsms.rb
CHANGED
|
@@ -7,12 +7,14 @@
|
|
|
7
7
|
#
|
|
8
8
|
# sms = KwtSMS::Client.from_env # reads .env / env vars
|
|
9
9
|
# ok, balance, err = sms.verify
|
|
10
|
-
# result = sms.send_sms("
|
|
11
|
-
# result = sms.send_sms("
|
|
10
|
+
# result = sms.send_sms("965XXXXXXXX", "Your OTP is: 123456")
|
|
11
|
+
# result = sms.send_sms("965XXXXXXXX", "Hello", sender: "MY-APP")
|
|
12
12
|
# balance = sms.balance
|
|
13
13
|
#
|
|
14
14
|
# Utility functions:
|
|
15
|
-
# KwtSMS.normalize_phone("+965
|
|
15
|
+
# KwtSMS.normalize_phone("+965 XXXX XXXX")
|
|
16
|
+
#
|
|
17
|
+
# 2026-11-05 08:07 +03: placeholder instead of a sample mobile number (it belongs to a real person), int-testnumber.
|
|
16
18
|
# KwtSMS.validate_phone_input("user@email.com")
|
|
17
19
|
# KwtSMS.clean_message("Hello \u{1F600} world")
|
|
18
20
|
|
metadata
CHANGED
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: kwtsms
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
|
-
-
|
|
7
|
+
- kwtSMS
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-11-07 00:00:00.000000000 Z
|
|
12
12
|
dependencies: []
|
|
13
13
|
description: Official Ruby client library for the kwtSMS SMS gateway. Send SMS, check
|
|
14
14
|
balance, validate phone numbers, check delivery status, and manage sender IDs. Zero
|
|
15
15
|
external dependencies.
|
|
16
16
|
email:
|
|
17
|
-
-
|
|
17
|
+
- support@kwtsms.com
|
|
18
18
|
executables: []
|
|
19
19
|
extensions: []
|
|
20
20
|
extra_rdoc_files: []
|
|
@@ -31,15 +31,15 @@ files:
|
|
|
31
31
|
- lib/kwtsms/phone.rb
|
|
32
32
|
- lib/kwtsms/request.rb
|
|
33
33
|
- lib/kwtsms/version.rb
|
|
34
|
-
homepage: https://github.com/
|
|
34
|
+
homepage: https://github.com/kwtsms/kwtsms-ruby
|
|
35
35
|
licenses:
|
|
36
36
|
- MIT
|
|
37
37
|
metadata:
|
|
38
|
-
homepage_uri: https://github.com/
|
|
39
|
-
source_code_uri: https://github.com/
|
|
40
|
-
changelog_uri: https://github.com/
|
|
41
|
-
bug_tracker_uri: https://github.com/
|
|
42
|
-
documentation_uri: https://github.com/
|
|
38
|
+
homepage_uri: https://github.com/kwtsms/kwtsms-ruby
|
|
39
|
+
source_code_uri: https://github.com/kwtsms/kwtsms-ruby
|
|
40
|
+
changelog_uri: https://github.com/kwtsms/kwtsms-ruby/blob/main/CHANGELOG.md
|
|
41
|
+
bug_tracker_uri: https://github.com/kwtsms/kwtsms-ruby/issues
|
|
42
|
+
documentation_uri: https://github.com/kwtsms/kwtsms-ruby#readme
|
|
43
43
|
rubygems_mfa_required: 'true'
|
|
44
44
|
post_install_message:
|
|
45
45
|
rdoc_options: []
|