Ruby client for the kwtSMS API. Send SMS, check balance, validate numbers, list sender IDs, and check coverage.
kwtSMS is a Kuwaiti SMS gateway trusted by top businesses to deliver messages anywhere in the world, with private Sender ID, free API testing, non-expiring credits, and competitive flat-rate pricing. Secure, simple to integrate, built to last. Open a free account in under 1 minute, no paperwork or payment required. Click here to get started
You need Ruby (>= 2.7) installed.
ruby -vIf not installed, see ruby-lang.org/en/downloads.
gem install kwtsmsOr add to your Gemfile:
gem "kwtsms"require "kwtsms"
sms = KwtSMS::Client.from_env
# Verify credentials
ok, balance, err = sms.verify
puts "Balance: #{balance}" if ok
# Send SMS
result = sms.send_sms("96598765432", "Your OTP for MyApp is: 123456")
puts "msg-id: #{result['msg-id']}" if result["result"] == "OK"Create a .env file or set environment variables:
KWTSMS_USERNAME=ruby_username
KWTSMS_PASSWORD=ruby_password
KWTSMS_SENDER_ID=YOUR-SENDER
KWTSMS_TEST_MODE=1
KWTSMS_LOG_FILE=kwtsms.logsms = KwtSMS::Client.new(
"ruby_username",
"ruby_password",
sender_id: "YOUR-SENDER",
test_mode: true,
log_file: "kwtsms.log"
)Test credentials and check balance.
ok, balance, err = sms.verify
# ok: true/false
# balance: Float or nil
# err: nil or error message stringGet current balance.
balance = sms.balance # Float or nilSend SMS to one or more numbers.
# Single number
result = sms.send_sms("96598765432", "Hello!")
# Multiple numbers
result = sms.send_sms(["96598765432", "96512345678"], "Bulk message")
# Override sender ID
result = sms.send_sms("96598765432", "Hello!", sender: "MY-APP")Response on success:
{
"result" => "OK",
"msg-id" => "12345",
"numbers" => 1,
"points-charged" => 1,
"balance-after" => 149.0
}Never call balance after send_sms. The send response already includes your updated balance in balance-after.
Send with automatic retry on ERR028 (15-second rate limit).
result = sms.send_with_retry("96598765432", "Hello!", max_retries: 3)List sender IDs registered on your account.
result = sms.senderids
puts result["senderids"] # => ["KWT-SMS", "MY-APP"]List active country prefixes.
result = sms.coverageValidate phone numbers.
result = sms.validate(["96598765432", "invalid", "+96512345678"])
puts result["ok"] # valid numbers
puts result["er"] # error numbers
puts result["rejected"] # locally rejected with error messages# Normalize phone number: Arabic digits, strip non-digits, strip leading zeros
KwtSMS.normalize_phone("+965 9876 5432") # => "96598765432"
# Validate phone input (returns [valid, error, normalized])
valid, error, normalized = KwtSMS.validate_phone_input("user@email.com")
# => [false, "'user@email.com' is an email address, not a phone number", ""]
# Clean message: strip emojis, HTML, hidden chars, convert Arabic digits
KwtSMS.clean_message("Hello \u{1F600} <b>world</b>") # => "Hello world"
# Enrich error with developer-friendly action message
KwtSMS.enrich_error({"result" => "ERROR", "code" => "ERR003"})
# => adds "action" key with guidance
# Access all error codes
KwtSMS::API_ERRORS # => Hash of all error codes with action messagesWhen passing more than 200 numbers to send_sms, the library automatically:
- Splits into batches of 200
- Sends each batch with a 0.5s delay
- Retries ERR013 (queue full) up to 3 times with 30s/60s/120s backoff
- Returns aggregated result:
OK,PARTIAL, orERROR
result = sms.send_sms(large_number_list, "Announcement")
puts result["batches"] # number of batches
puts result["msg-ids"] # array of message IDs
puts result["points-charged"] # total points
puts result["balance-after"] # final balance
puts result["errors"] # any batch errorsAll formats are accepted and normalized automatically:
| Input | Normalized | Valid? |
|---|---|---|
96598765432 |
96598765432 |
Yes |
+96598765432 |
96598765432 |
Yes |
0096598765432 |
96598765432 |
Yes |
965 9876 5432 |
96598765432 |
Yes |
965-9876-5432 |
96598765432 |
Yes |
(965) 98765432 |
96598765432 |
Yes |
٩٦٥٩٨٧٦٥٤٣٢ |
96598765432 |
Yes |
۹۶۵۹۸۷۶۵۴۳۲ |
96598765432 |
Yes |
+٩٦٥٩٨٧٦٥٤٣٢ |
96598765432 |
Yes |
٠٠٩٦٥٩٨٧٦٥٤٣٢ |
96598765432 |
Yes |
٩٦٥ ٩٨٧٦ ٥٤٣٢ |
96598765432 |
Yes |
٩٦٥-٩٨٧٦-٥٤٣٢ |
96598765432 |
Yes |
965٩٨٧٦٥٤٣٢ |
96598765432 |
Yes |
123456 (too short) |
rejected | No |
user@gmail.com |
rejected | No |
Normalization rules:
- Arabic-Indic and Extended Arabic-Indic digits converted to Latin
- Non-digit characters stripped (
+, spaces, dashes, dots, brackets) - Leading zeros stripped (handles
00country code prefix) - Duplicate numbers deduplicated before sending
- Invalid numbers rejected locally with clear error messages
Messages are cleaned automatically before sending to prevent silent delivery failures:
- Emojis stripped (cause messages to get stuck in queue)
- HTML tags stripped (causes ERR027)
- Hidden characters stripped (BOM, zero-width spaces, soft hyphens, directional marks)
- Arabic-Indic digits converted to Latin
- C0/C1 control characters removed (except
\nand\t)
For a standalone CLI tool that works on all operating systems, see kwtsms-cli.
Never hardcode credentials in source code. Credentials must be changeable without recompiling or redeploying.
sms = KwtSMS::Client.from_env # reads KWTSMS_USERNAME, KWTSMS_PASSWORD, etc.# config/initializers/kwtsms.rb
KWTSMS_CLIENT = KwtSMS::Client.from_envsms = KwtSMS::Client.new(
config[:username],
config[:password],
sender_id: config[:sender_id]
)valid, error, normalized = KwtSMS.validate_phone_input(user_input)
unless valid
# Don't waste an API call on invalid input
return { error: error }
end
result = sms.send_sms(normalized, message)Never expose raw API errors to end users:
| Situation | API Code | Show to User |
|---|---|---|
| Invalid phone | ERR006, ERR025 | "Please enter a valid phone number in international format." |
| Auth error | ERR003 | "SMS service temporarily unavailable. Please try again later." |
| No balance | ERR010, ERR011 | "SMS service temporarily unavailable. Please try again later." |
| Rate limited | ERR028 | "Please wait a moment before requesting another code." |
| Content rejected | ERR031, ERR032 | "Your message could not be sent. Please try again with different content." |
KWT-SMSis a shared test sender: delays, blocked on some carriers. Never use in production.- Register a private Sender ID at kwtsms.com (takes ~5 working days for Kuwait).
- Sender ID is case sensitive:
Kuwaitis not the same asKUWAIT. - For OTP, use Transactional Sender ID. Promotional IDs are filtered by DND on Zain and Ooredoo.
unix-timestamp in API responses is GMT+3 (Asia/Kuwait server time), not UTC. Always convert when storing or displaying. Log timestamps written by this client are UTC.
Before going live:
- Bot protection enabled (CAPTCHA for web)
- Rate limit per phone number (max 3-5/hour)
- Rate limit per IP address (max 10-20/hour)
- Rate limit per user/session if authenticated
- Monitoring/alerting on abuse patterns
- Admin notification on low balance
- Test mode OFF (
KWTSMS_TEST_MODE=0) - Private Sender ID registered (not KWT-SMS)
- Transactional Sender ID for OTP (not promotional)
Every API call is logged to a JSONL file (default: kwtsms.log):
{"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}Passwords are always masked as ***. Logging never crashes the main flow.
Disable logging by setting log_file: "" in the constructor.
See the examples/ directory:
| # | Example | Description |
|---|---|---|
| 00 | Raw API | Call all 7 endpoints directly, no gem needed |
| 01 | Basic Usage | Connect, verify, send SMS, validate |
| 02 | OTP Flow | Send OTP codes |
| 03 | Bulk SMS | Send to many recipients |
| 04 | Rails Endpoint | Rails controller |
| 05 | Error Handling | Handle every error type |
| 06 | OTP Production | Production OTP with rate limiting, CAPTCHA, Redis |
- Ruby >= 2.7
- No external runtime dependencies
# 1. Create account at https://rubygems.org/sign_up
# 2. Build the gem
gem build kwtsms.gemspec
# 3. Push to RubyGems
gem push kwtsms-0.1.0.gem
# 4. Or use the automated GitHub Actions workflow:
# Push a tag and it publishes automatically
git tag v0.1.0
git push origin v0.1.01. My message was sent successfully (result: OK) but the recipient didn't receive it. What happened?
Check the Sending Queue at kwtsms.com. If your message is stuck there, it was accepted by the API but not dispatched. Common causes are emoji in the message, hidden characters from copy-pasting, or spam filter triggers. Delete it from the queue to recover your credits. Also verify that test mode is off (KWTSMS_TEST_MODE=0). Test messages are queued but never delivered.
2. What is the difference between Test mode and Live mode?
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.
3. What is a Sender ID and why should I not use "KWT-SMS" in production?
A Sender ID is the name that appears as the sender on the recipient's phone (e.g., "MY-APP" instead of a random number). KWT-SMS is a shared test sender. It causes delivery delays, is blocked on Virgin Kuwait, and should never be used in production. Register your own private Sender ID through your kwtSMS account. For OTP/authentication messages, you need a Transactional Sender ID to bypass DND (Do Not Disturb) filtering.
4. I'm getting ERR003 "Authentication error". What's wrong?
You are using the wrong credentials. The API requires your API username and API password, NOT your account mobile number. Log in to kwtsms.com, go to Account, and check your API credentials. Also make sure you are using POST (not GET) and Content-Type: application/json.
5. Can I send to international numbers (outside Kuwait)?
International sending is disabled by default on kwtSMS accounts. Log in to your kwtSMS dashboard and add coverage for the country prefixes you need. Use coverage() to check which countries are currently active on your account. Be aware that activating international coverage increases exposure to automated abuse. Implement rate limiting and CAPTCHA before enabling.
- kwtSMS FAQ: Answers to common questions about credits, sender IDs, OTP, and delivery
- kwtSMS Support: Open a support ticket or browse help articles
- Contact kwtSMS: Reach the kwtSMS team directly for Sender ID registration and account issues
- API Documentation (PDF): kwtSMS REST API v4.1 full reference
- Best Practices: SMS API implementation best practices
- Integration Test Checklist: Pre-launch testing checklist
- Sender ID Help: Sender ID registration and troubleshooting guide
- kwtSMS Dashboard: Recharge credits, buy Sender IDs, view message logs, manage coverage
- Other Integrations: Plugins and integrations for other platforms and languages
- RubyGems: Package on RubyGems.org
- GitHub: Source code and issue tracker
- Changelog | Contributing | Security
MIT License. See LICENSE.