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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 93dfa6c3d929f5f5767a1dd561078bf3d2e099dfbc8aea04e9d5f0ffa81fbf6a
4
- data.tar.gz: 4b0c380d47b356060cbc976a1d035c167b03718572f88e2417cbd9f74d5137cd
3
+ metadata.gz: 8d0309851d3d99cd00dc43e57fcc1a6a3c325573a3ef5f03cc605b6155e2f8c5
4
+ data.tar.gz: ded82b65342f4b32e9aa42ced78bcdeeeb496efb2865b543ec4dfef1d8e8bc46
5
5
  SHA512:
6
- metadata.gz: 02ef8c6d2c00049be8195aab4b359cbbd0d567c42d77f092ac50d31cb66cd021479b95ad51d46c8fae5205a0bd62b82756f6a6fd11bec40248639974a5dc7a29
7
- data.tar.gz: 35bf19319f4548a648d96c2802d1176b1e5ed1c2279d4b5d29dcbcedc155b25d94fc50464165aa1c9cf4d6756566b7a0314785efd28c3e6cc36d01445e096976
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
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2026 boxlink
3
+ Copyright (c) 2026 kwtSMS
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
data/README.md CHANGED
@@ -2,11 +2,11 @@
2
2
 
3
3
  [![Gem Version](https://img.shields.io/gem/v/kwtsms.svg)](https://rubygems.org/gems/kwtsms)
4
4
  [![Gem Downloads](https://img.shields.io/gem/dt/kwtsms.svg)](https://rubygems.org/gems/kwtsms)
5
- [![CI](https://github.com/boxlinknet/kwtsms-ruby/actions/workflows/ci.yml/badge.svg)](https://github.com/boxlinknet/kwtsms-ruby/actions/workflows/ci.yml)
6
- [![CodeQL](https://github.com/boxlinknet/kwtsms-ruby/actions/workflows/codeql.yml/badge.svg)](https://github.com/boxlinknet/kwtsms-ruby/actions/workflows/codeql.yml)
7
- [![Bundle Audit](https://github.com/boxlinknet/kwtsms-ruby/actions/workflows/audit.yml/badge.svg)](https://github.com/boxlinknet/kwtsms-ruby/actions/workflows/audit.yml)
8
- [![GitGuardian](https://github.com/boxlinknet/kwtsms-ruby/actions/workflows/gitguardian.yml/badge.svg)](https://github.com/boxlinknet/kwtsms-ruby/actions/workflows/gitguardian.yml)
9
- [![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/boxlinknet/kwtsms-ruby/badge)](https://scorecard.dev/viewer/?uri=github.com/boxlinknet/kwtsms-ruby)
5
+ [![CI](https://github.com/kwtsms/kwtsms-ruby/actions/workflows/ci.yml/badge.svg)](https://github.com/kwtsms/kwtsms-ruby/actions/workflows/ci.yml)
6
+ [![CodeQL](https://github.com/kwtsms/kwtsms-ruby/actions/workflows/codeql.yml/badge.svg)](https://github.com/kwtsms/kwtsms-ruby/actions/workflows/codeql.yml)
7
+ [![Bundle Audit](https://github.com/kwtsms/kwtsms-ruby/actions/workflows/audit.yml/badge.svg)](https://github.com/kwtsms/kwtsms-ruby/actions/workflows/audit.yml)
8
+ [![GitGuardian](https://github.com/kwtsms/kwtsms-ruby/actions/workflows/gitguardian.yml/badge.svg)](https://github.com/kwtsms/kwtsms-ruby/actions/workflows/gitguardian.yml)
9
+ [![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/kwtsms/kwtsms-ruby/badge)](https://scorecard.dev/viewer/?uri=github.com/kwtsms/kwtsms-ruby)
10
10
  [![Ruby](https://img.shields.io/badge/Ruby-%3E%3D%202.7-red.svg)](https://www.ruby-lang.org/)
11
11
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](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("96598765432", "Your OTP for MyApp is: 123456")
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("96598765432", "Hello!")
122
+ result = sms.send_sms("965XXXXXXXX", "Hello!")
113
123
 
114
124
  # Multiple numbers
115
- result = sms.send_sms(["96598765432", "96512345678"], "Bulk message")
125
+ result = sms.send_sms(["965XXXXXXXX", "96512345678"], "Bulk message")
116
126
 
117
127
  # Override sender ID
118
- result = sms.send_sms("96598765432", "Hello!", sender: "MY-APP")
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 automatic retry on ERR028 (15-second rate limit).
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("96598765432", "Hello!", max_retries: 3)
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(["96598765432", "invalid", "+96512345678"])
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 9876 5432") # => "96598765432"
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
- | `96598765432` | `96598765432` | Yes |
216
- | `+96598765432` | `96598765432` | Yes |
217
- | `0096598765432` | `96598765432` | Yes |
218
- | `965 9876 5432` | `96598765432` | Yes |
219
- | `965-9876-5432` | `96598765432` | Yes |
220
- | `(965) 98765432` | `96598765432` | Yes |
221
- | `٩٦٥٩٨٧٦٥٤٣٢` | `96598765432` | Yes |
222
- | `۹۶۵۹۸۷۶۵۴۳۲` | `96598765432` | Yes |
223
- | `+٩٦٥٩٨٧٦٥٤٣٢` | `96598765432` | Yes |
224
- | `٠٠٩٦٥٩٨٧٦٥٤٣٢` | `96598765432` | Yes |
225
- | `٩٦٥ ٩٨٧٦ ٥٤٣٢` | `96598765432` | Yes |
226
- | `٩٦٥-٩٨٧٦-٥٤٣٢` | `96598765432` | Yes |
227
- | `965٩٨٧٦٥٤٣٢` | `96598765432` | Yes |
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/boxlinknet/kwtsms-cli).
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":"96598765432","message":"Hello"},"response":{"result":"OK","msg-id":"12345"},"ok":true,"error":null}
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. No SMS credits are consumed. 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.
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/boxlinknet/kwtsms-ruby)**: Source code and issue tracker
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("96598765432", "Your OTP for MYAPP is: 123456")
23
- # result = sms.send_sms("96598765432", "Hello", sender: "OTHER-ID")
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
- def initialize(username, password, sender_id: "KWT-SMS", test_mode: false, log_file: "kwtsms.log")
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 automatically on ERR028 (rate limit: wait 15 seconds).
291
+ # Send SMS, optionally retrying on ERR028 (duplicate SMS protection).
286
292
  #
287
- # Waits 16 seconds between retries (15s required + 1s buffer).
288
- # All other errors are returned immediately without retry.
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 3)
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: 3)
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(16)
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
- "ERR006" => "No valid phone numbers. Make sure each number includes the country code (e.g., 96598765432 for Kuwait, not 98765432).",
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., 96598765432 for Kuwait, not 98765432).",
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
- "ERR028" => "You must wait at least 15 seconds before sending to the same number again. No credits were consumed.",
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.",
@@ -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
- def self.api_request(endpoint, payload, log_file = "")
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)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module KwtSMS
4
- VERSION = "0.3.1"
4
+ VERSION = "0.4.0"
5
5
  end
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("96598765432", "Your OTP is: 123456")
11
- # result = sms.send_sms("96598765432", "Hello", sender: "MY-APP")
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 9876 5432")
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.3.1
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
- - boxlink
7
+ - kwtSMS
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-03-15 00:00:00.000000000 Z
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
- - info@boxlink.net
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/boxlinknet/kwtsms-ruby
34
+ homepage: https://github.com/kwtsms/kwtsms-ruby
35
35
  licenses:
36
36
  - MIT
37
37
  metadata:
38
- homepage_uri: https://github.com/boxlinknet/kwtsms-ruby
39
- source_code_uri: https://github.com/boxlinknet/kwtsms-ruby
40
- changelog_uri: https://github.com/boxlinknet/kwtsms-ruby/blob/main/CHANGELOG.md
41
- bug_tracker_uri: https://github.com/boxlinknet/kwtsms-ruby/issues
42
- documentation_uri: https://github.com/boxlinknet/kwtsms-ruby#readme
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: []