{"id":49888293,"url":"https://github.com/boxlinknet/kwtsms-go","last_synced_at":"2026-05-15T19:33:48.879Z","repository":{"id":342438912,"uuid":"1173963084","full_name":"boxlinknet/kwtsms-go","owner":"boxlinknet","description":"Go client library for the kwtSMS SMS API (kwtsms.com). Zero dependencies. Send SMS, check balance, validate numbers, and more.","archived":false,"fork":false,"pushed_at":"2026-03-13T20:31:06.000Z","size":132,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-03-14T07:45:50.291Z","etag":null,"topics":["client-library","go","golang","kuwait","kwtsms","otp","sdk","sms","sms-api","sms-gateway"],"latest_commit_sha":null,"homepage":"https://www.kwtsms.com/developers.html","language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/boxlinknet.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":"SECURITY.md","support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-03-05T23:42:13.000Z","updated_at":"2026-03-13T20:31:11.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/boxlinknet/kwtsms-go","commit_stats":null,"previous_names":["boxlinknet/kwtsms-go"],"tags_count":2,"template":false,"template_full_name":null,"purl":"pkg:github/boxlinknet/kwtsms-go","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/boxlinknet%2Fkwtsms-go","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/boxlinknet%2Fkwtsms-go/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/boxlinknet%2Fkwtsms-go/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/boxlinknet%2Fkwtsms-go/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/boxlinknet","download_url":"https://codeload.github.com/boxlinknet/kwtsms-go/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/boxlinknet%2Fkwtsms-go/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33076221,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-15T11:35:32.926Z","status":"ssl_error","status_checked_at":"2026-05-15T11:35:31.362Z","response_time":103,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.5:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["client-library","go","golang","kuwait","kwtsms","otp","sdk","sms","sms-api","sms-gateway"],"created_at":"2026-05-15T19:33:47.505Z","updated_at":"2026-05-15T19:33:48.843Z","avatar_url":"https://github.com/boxlinknet.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"# kwtSMS Go Client\n\n[![Go Reference](https://pkg.go.dev/badge/github.com/boxlinknet/kwtsms-go.svg)](https://pkg.go.dev/github.com/boxlinknet/kwtsms-go)\n[![CI](https://github.com/boxlinknet/kwtsms-go/actions/workflows/ci.yml/badge.svg)](https://github.com/boxlinknet/kwtsms-go/actions/workflows/ci.yml)\n[![CodeQL](https://github.com/boxlinknet/kwtsms-go/actions/workflows/codeql.yml/badge.svg)](https://github.com/boxlinknet/kwtsms-go/actions/workflows/codeql.yml)\n[![Go Report Card](https://goreportcard.com/badge/github.com/boxlinknet/kwtsms-go)](https://goreportcard.com/report/github.com/boxlinknet/kwtsms-go)\n[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)\n[![Go Version](https://img.shields.io/github/go-mod/go-version/boxlinknet/kwtsms-go)](https://github.com/boxlinknet/kwtsms-go)\n[![Release](https://img.shields.io/github/v/release/boxlinknet/kwtsms-go)](https://github.com/boxlinknet/kwtsms-go/releases)\n\nGo client library for the [kwtSMS SMS API](https://www.kwtsms.com). Zero external dependencies. Go 1.18+.\n\n## About kwtSMS\n\nkwtSMS 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](https://www.kwtsms.com/signup/)\n\n## Prerequisites\n\nYou need **Go** (1.18 or newer) installed.\n\n### Step 1: Check if Go is installed\n\n```bash\ngo version\n```\n\nIf you see a version number, Go is installed. If not:\n\n- **All platforms:** Download from https://go.dev/dl/\n- **macOS:** `brew install go`\n- **Ubuntu/Debian:** `sudo apt update \u0026\u0026 sudo apt install golang-go`\n\n### Step 2: Create a project (if you don't have one)\n\n```bash\nmkdir my-project \u0026\u0026 cd my-project\ngo mod init my-project\n```\n\n### Step 3: Install kwtsms\n\n```bash\ngo get github.com/boxlinknet/kwtsms-go\n```\n\n## Quick Start\n\n```go\npackage main\n\nimport (\n    \"fmt\"\n    \"log\"\n\n    kwtsms \"github.com/boxlinknet/kwtsms-go\"\n)\n\nfunc main() {\n    // Load credentials from environment variables or .env file\n    sms, err := kwtsms.FromEnv(\"\")\n    if err != nil {\n        log.Fatal(err)\n    }\n\n    // Verify credentials and check balance\n    ok, balance, err := sms.Verify()\n    if !ok {\n        log.Fatalf(\"verification failed: %v\", err)\n    }\n    fmt.Printf(\"Balance: %.2f credits\\n\", balance)\n\n    // Send an SMS\n    result, err := sms.Send(\"96598765432\", \"Your OTP for MYAPP is: 123456\", \"\")\n    if err != nil {\n        log.Fatal(err)\n    }\n    if result.Result == \"OK\" {\n        fmt.Printf(\"Sent! msg-id: %s, balance: %.2f\\n\", result.MsgID, result.BalanceAfter)\n    } else {\n        fmt.Printf(\"Error: %s\\nAction: %s\\n\", result.Description, result.Action)\n    }\n}\n```\n\n## Setup / Configuration\n\n### CLI Tool\n\nFor a standalone command-line tool (all platforms), see [kwtsms-cli](https://github.com/boxlinknet/kwtsms-cli).\n\n### Environment variables\n\nSet these environment variables or create a `.env` file in your project root:\n\n```ini\nKWTSMS_USERNAME=go_api_user\nKWTSMS_PASSWORD=go_api_pass\nKWTSMS_SENDER_ID=YOUR-SENDERID\nKWTSMS_TEST_MODE=1\nKWTSMS_LOG_FILE=kwtsms.log\n```\n\n### Constructor\n\n```go\n// From environment variables / .env file (recommended)\nsms, err := kwtsms.FromEnv(\"\")\n\n// From .env at a custom path\nsms, err := kwtsms.FromEnv(\"/path/to/.env\")\n\n// Direct constructor with options\nsms, err := kwtsms.New(\"username\", \"password\",\n    kwtsms.WithSenderID(\"MY-APP\"),\n    kwtsms.WithTestMode(true),\n    kwtsms.WithLogFile(\"sms.log\"),\n)\n```\n\nEnvironment variables take priority over `.env` file values.\n\n## Credential Management\n\n**Never hardcode credentials in source code.** Credentials must be changeable without recompiling.\n\n### Environment variables / .env file (recommended for servers)\n\n```go\nsms, err := kwtsms.FromEnv(\"\")  // reads KWTSMS_USERNAME, KWTSMS_PASSWORD\n```\n\nThe `.env` file must be in `.gitignore`. Never commit credentials.\n\n### Constructor injection (for custom config systems)\n\n```go\nsms, err := kwtsms.New(\n    config.Get(\"sms_username\"),\n    config.Get(\"sms_password\"),\n)\n```\n\nWorks with any config source: Vault, AWS Secrets Manager, database, DI containers, etc.\n\n### Admin settings UI (recommended for web apps)\n\nProvide a settings page where an admin can enter API credentials and toggle test mode. Include a \"Test Connection\" button that calls `Verify()`.\n\n### Remote config / secrets manager (recommended for production)\n\nLoad credentials from AWS Secrets Manager, Google Secret Manager, HashiCorp Vault, or your own config API. Credentials rotate without redeployment.\n\n## API Reference\n\n### Verify Credentials\n\n```go\nok, balance, err := sms.Verify()\n// ok=true:  credentials valid, balance is the available credit count\n// ok=false: err describes the problem with an action to take\n```\n\n### Check Balance\n\n```go\nbalance, err := sms.Balance()\n// Returns live balance, or cached value if the API call fails\n```\n\n### Send SMS\n\n```go\n// Single number\nresult, err := sms.Send(\"96598765432\", \"Hello from Go!\", \"\")\n\n// Multiple numbers (comma-separated)\nresult, err := sms.Send(\"96598765432,96512345678\", \"Bulk message\", \"\")\n\n// Multiple numbers (slice)\nresult, err := sms.SendMulti(\n    []string{\"96598765432\", \"+96512345678\", \"0096587654321\"},\n    \"Hello everyone!\",\n    \"\",\n)\n\n// Override sender ID for one call\nresult, err := sms.Send(\"96598765432\", \"Hello\", \"OTHER-SENDER\")\n```\n\n**Always save `msg-id` immediately after a successful send.** You need it for status checks and delivery reports. If you do not store it at send time, you cannot retrieve it later.\n\n**Never call `Balance()` after `Send()`.** The send response already includes your updated balance in `result.BalanceAfter`. Save it to your database. The client also caches it internally.\n\n### Send with Retry (ERR028)\n\n```go\n// Auto-retries on ERR028 (15-second rate limit), waits 16s between retries\nresult, err := sms.SendWithRetry(\"96598765432\", \"Hello\", \"\", 3)\n```\n\n### Bulk Send (\u003e200 numbers)\n\nSending to more than 200 numbers is handled automatically. The client splits numbers into batches of 200, adds a 0.5s delay between batches, and retries on ERR013 (queue full) with 30s/60s/120s backoff.\n\n```go\nnumbers := make([]string, 500)\n// ... populate numbers ...\nresult, err := sms.SendMulti(numbers, \"Campaign message\", \"MY-SENDER\")\n```\n\n### Validate Phone Numbers\n\n```go\nresult := sms.Validate([]string{\"96598765432\", \"+96512345678\", \"bad@email.com\", \"123\"})\nfmt.Println(\"Valid:\", result.OK)       // valid and routable\nfmt.Println(\"Errors:\", result.ER)      // format errors\nfmt.Println(\"No route:\", result.NR)    // country not activated\nfmt.Println(\"Rejected:\", result.Rejected) // locally rejected (email, too short, etc.)\n```\n\nNumbers that fail local validation (empty, email, too short, too long, no digits) are rejected before the API call is made.\n\n### Sender IDs\n\n```go\nresult := sms.SenderIDs()\nif result[\"result\"] == \"OK\" {\n    sids := result[\"senderids\"].([]string)\n    fmt.Println(\"Sender IDs:\", sids)\n}\n```\n\n### Coverage\n\n```go\nresult := sms.Coverage()\nif result[\"result\"] == \"OK\" {\n    fmt.Println(\"Active coverage:\", result)\n}\n```\n\n### Message Status\n\n```go\nresult := sms.Status(\"msg-id-from-send-response\")\nfmt.Println(result)\n```\n\n### Delivery Report (international only)\n\n```go\nresult := sms.DLR(\"msg-id-from-send-response\")\nfmt.Println(result)\n```\n\nKuwait numbers do not support DLR. Only international (non-Kuwait) numbers have delivery reports. Wait at least 5 minutes after sending before checking.\n\n### Cached Balance\n\n```go\n// Available after Verify() or successful Send()\nif bal := sms.CachedBalance(); bal != nil {\n    fmt.Printf(\"Cached balance: %.2f\\n\", *bal)\n}\n\n// Total purchased credits (available after Verify())\nif p := sms.CachedPurchased(); p != nil {\n    fmt.Printf(\"Purchased: %.2f\\n\", *p)\n}\n```\n\n## Utility Functions\n\nThese are exported for direct use:\n\n```go\n// Normalize a phone number: Arabic digits to Latin, strip non-digits, strip leading zeros,\n// strip domestic trunk prefix (e.g. 9660559... becomes 966559...)\nnormalized := kwtsms.NormalizePhone(\"+965 9876 5432\") // \"96598765432\"\nnormalized = kwtsms.NormalizePhone(\"9660559876543\")   // \"966559876543\" (Saudi trunk 0 stripped)\n\n// Validate phone input before sending (includes country-specific rules)\nv := kwtsms.ValidatePhoneInput(\"user@gmail.com\")\n// v.Valid=false, v.Error=\"'user@gmail.com' is an email address, not a phone number\"\n\nv = kwtsms.ValidatePhoneInput(\"+96598765432\")\n// v.Valid=true, v.Normalized=\"96598765432\"\n\nv = kwtsms.ValidatePhoneInput(\"+96512345678\")\n// v.Valid=false, v.Error=\"Invalid Kuwait mobile number: after +965 must start with 4, 5, 6, 9\"\n\n// Find the country code from a normalized number\ncc := kwtsms.FindCountryCode(\"96598765432\") // \"965\" (Kuwait)\ncc = kwtsms.FindCountryCode(\"12125551234\")  // \"1\" (USA/Canada)\n\n// Validate against country-specific format rules\nerr := kwtsms.ValidatePhoneFormat(\"96598765432\") // \"\" (valid)\nerr = kwtsms.ValidatePhoneFormat(\"96512345678\")  // \"Invalid Kuwait mobile number: ...\"\n\n// Look up phone rules for a country (90+ countries covered)\nrule := kwtsms.PhoneRules[\"965\"] // {LocalLengths: [8], MobileStartDigits: [\"4\",\"5\",\"6\",\"9\"]}\nname := kwtsms.CountryNames[\"965\"] // \"Kuwait\"\n\n// Load .env file manually (returns key-value map, never panics)\nenv := kwtsms.LoadEnvFile(\"/path/to/.env\")\n\n// Clean a message: strip emojis, HTML, control chars, convert Arabic digits\ncleaned := kwtsms.CleanMessage(\"Hello  \u003cb\u003eWorld\u003c/b\u003e 123\")\n// \"Hello  World 123\"\n\n// API error map (for building custom error UIs)\naction := kwtsms.APIErrors[\"ERR003\"]\n// \"Wrong API username or password. Check KWTSMS_USERNAME and KWTSMS_PASSWORD...\"\n\n// Enrich an error response with action guidance\nenriched := kwtsms.EnrichError(apiResponse)\n```\n\n## Phone Validation Rules\n\n`ValidatePhoneInput()` and `Send()` automatically validate numbers against country-specific rules for 90+ countries. Validation checks:\n\n1. **Local number length**: each country has specific valid lengths (e.g., Kuwait: 8 digits after +965)\n2. **Mobile prefix**: each country has valid starting digits for mobile numbers (e.g., Kuwait: 4, 5, 6, 9)\n3. **Domestic trunk prefix**: automatically stripped during normalization (e.g., 9660559... becomes 966559...)\n\nNumbers from countries not in the rules table pass through with generic E.164 validation (7-15 digits).\n\nGCC coverage:\n\n| Country | Code | Local Digits | Mobile Prefixes |\n|---------|------|-------------|-----------------|\n| Kuwait | 965 | 8 | 4x, 5x, 6x, 9x |\n| Saudi Arabia | 966 | 9 | 5x |\n| UAE | 971 | 9 | 5x |\n| Bahrain | 973 | 8 | 3x, 6x |\n| Qatar | 974 | 8 | 3x, 5x, 6x, 7x |\n| Oman | 968 | 8 | 7x, 9x |\n\nThe full rules table (`kwtsms.PhoneRules`) covers GCC, Levant, North Africa, Europe, Americas, Asia, Africa, and Oceania. See `phone.go` for the complete list.\n\n## Input Sanitization\n\nThe `CleanMessage()` function runs automatically before every send. It prevents the most common cause of \"message sent but not received\" support tickets:\n\n| Content | Problem | Fix |\n|---------|---------|-----|\n| Emojis | Message stuck in queue indefinitely, credits wasted, no error returned | Stripped before send |\n| Hidden control characters (BOM, zero-width spaces, soft hyphens) | Spam filter rejection or queue stuck, common in text from Word/PDF/rich editors | Stripped before send |\n| Arabic/Hindi numerals in body | OTP codes and amounts may render inconsistently | Converted to Latin digits |\n| HTML tags | ERR027, message rejected | Stripped before send |\n| C0/C1 control characters | Unprintable binary from copy-pasting terminals or binary content | Stripped (except newlines and tabs) |\n| Directional marks (LTR, RTL, LRE, etc.) | Introduced by rich-text editors and RTL-aware apps | Stripped before send |\n\nArabic letters and Arabic text are fully preserved. Only digits are converted, invisible characters are removed, and emojis are stripped.\n\n`Send()` calls `CleanMessage()` automatically, but you can also call it directly to preview what the API will receive:\n\n```go\ncleaned := kwtsms.CleanMessage(userInput)\nif strings.TrimSpace(cleaned) == \"\" {\n    // Message was only emojis or control characters\n    return fmt.Errorf(\"message is empty after cleaning\")\n}\n```\n\n## Exports\n\n### Types\n\n```go\nkwtsms.KwtSMS           // Client struct\nkwtsms.SendResult       // Send response\nkwtsms.BulkSendResult   // Bulk send response (\u003e200 numbers)\nkwtsms.ValidateResult   // Validate response\nkwtsms.InvalidEntry     // Rejected phone number with error message\nkwtsms.BatchError       // Error from a single batch in bulk send\nkwtsms.PhoneValidation  // Result of ValidatePhoneInput\nkwtsms.PhoneRule        // Country-specific phone validation rule\nkwtsms.Option           // Functional option for New()\n```\n\n### Functions\n\n```go\nkwtsms.NormalizePhone()      // Normalize phone: Arabic digits, strip non-digits, trunk prefix\nkwtsms.ValidatePhoneInput()  // Validate phone input with country-specific rules\nkwtsms.FindCountryCode()     // Extract country code from normalized number\nkwtsms.ValidatePhoneFormat() // Validate against country-specific length and prefix rules\nkwtsms.CleanMessage()        // Strip emojis, HTML, control chars, convert Arabic digits\nkwtsms.EnrichError()         // Add action guidance to API error response\nkwtsms.LoadEnvFile()         // Parse .env file into key-value map\n```\n\n### Variables\n\n```go\nkwtsms.APIErrors     // map[string]string: error code to action message (33 codes)\nkwtsms.PhoneRules    // map[string]PhoneRule: country code to validation rules (90+ countries)\nkwtsms.CountryNames  // map[string]string: country code to human-readable name\n```\n\n## Error Handling\n\nEvery API error includes a developer-friendly `action` field explaining what to do. All 33 kwtSMS error codes are mapped.\n\n```go\nresult, _ := sms.Send(\"96598765432\", \"Hello\", \"\")\nif result.Result == \"ERROR\" {\n    fmt.Println(\"Code:\", result.Code)             // \"ERR003\"\n    fmt.Println(\"Description:\", result.Description) // \"Authentication error...\"\n    fmt.Println(\"Action:\", result.Action)           // \"Wrong API username or password. Check...\"\n}\n```\n\n### User-Facing Error Messages\n\nRaw API errors are for developers, not end users. Map them for your UI:\n\n| Situation | API error | Show to user |\n|-----------|-----------|--------------|\n| Invalid phone | ERR006, ERR025 | \"Please enter a valid phone number in international format (e.g., +965 9876 5432).\" |\n| Wrong credentials | ERR003 | \"SMS service is temporarily unavailable. Please try again later.\" |\n| No balance | ERR010, ERR011 | \"SMS service is temporarily unavailable. Please try again later.\" |\n| Country not supported | ERR026 | \"SMS delivery to this country is not available. Please contact support.\" |\n| Rate limited | ERR028 | \"Please wait a moment before requesting another code.\" |\n| Message rejected | ERR031, ERR032 | \"Your message could not be sent. Please try again with different content.\" |\n| Network error | timeout | \"Could not connect to SMS service. Check your internet connection.\" |\n| Queue full | ERR013 | \"SMS service is busy. Please try again in a few minutes.\" |\n\n**Key principle:** user-recoverable errors (bad phone, rate limited) get a helpful message. System-level errors (auth, balance, network) get a generic message + log the real error + alert the admin.\n\n## Phone Number Formats\n\nAll formats are accepted and normalized automatically:\n\n| Input | Normalized | Valid? |\n|-------|-----------|--------|\n| `96598765432` | `96598765432` | Yes |\n| `+96598765432` | `96598765432` | Yes |\n| `0096598765432` | `96598765432` | Yes |\n| `965 9876 5432` | `96598765432` | Yes |\n| `965-9876-5432` | `96598765432` | Yes |\n| `(965) 98765432` | `96598765432` | Yes |\n| `965.9876.5432` | `96598765432` | Yes |\n| `٩٦٥٩٨٧٦٥٤٣٢` | `96598765432` | Yes |\n| `۹۶۵۹۸۷۶۵۴۳۲` | `96598765432` | Yes |\n| `+٩٦٥٩٨٧٦٥٤٣٢` | `96598765432` | Yes |\n| `٠٠٩٦٥٩٨٧٦٥٤٣٢` | `96598765432` | Yes |\n| `٩٦٥ ٩٨٧٦ ٥٤٣٢` | `96598765432` | Yes |\n| `٩٦٥-٩٨٧٦-٥٤٣٢` | `96598765432` | Yes |\n| `965٩٨٧٦٥٤٣٢` | `96598765432` | Yes |\n| `123456` (too short) | rejected | No |\n| `user@gmail.com` | rejected | No |\n\nNumbers must be in international format with country code. Arabic-Indic (U+0660-U+0669) and Persian (U+06F0-U+06F9) digits are converted to Latin automatically.\n\n## Test Mode\n\n**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 during development and testing.\n\n**Live mode** (`KWTSMS_TEST_MODE=0`) delivers the message for real and deducts credits.\n\n```go\n// Enable test mode via constructor\nsms, _ := kwtsms.New(\"user\", \"pass\", kwtsms.WithTestMode(true))\n\n// Or via environment variable / .env file\n// KWTSMS_TEST_MODE=1\n```\n\nAlways develop in test mode and switch to live only when ready for production. Test messages appear in the **Sending Queue** at kwtsms.com. Delete them from the queue to recover any tentatively held credits.\n\n## Sender ID\n\nA **Sender ID** is the name that appears as the sender on the recipient's phone (e.g., \"MY-APP\" instead of a random number).\n\n| | Promotional | Transactional |\n|--|-------------|---------------|\n| **Use for** | Bulk SMS, marketing, offers | OTP, alerts, notifications |\n| **DND numbers** | Blocked/filtered, credits lost | Bypasses DND |\n| **Speed** | May have delays | Priority delivery |\n| **Cost** | 10 KD one-time | 15 KD one-time |\n\n`KWT-SMS` is the shared test sender. It causes delays and is blocked on Virgin Kuwait. **Never use in production.** Register a private sender ID at kwtsms.com.\n\nFor OTP/authentication, you **must** use a Transactional sender ID. Promotional sender IDs are filtered by DND (Do Not Disturb) on Zain and Ooredoo, meaning OTP messages silently fail and credits are still deducted.\n\nSender ID is **case sensitive**: `Kuwait` is not the same as `KUWAIT`.\n\nRegistration takes ~5 working days for Kuwait and 1-2 months for international.\n\n## Best Practices\n\n### 1. Validate before calling the API\n\nThe #1 cause of wasted API calls: sending invalid input and letting the API reject it. Validate locally first:\n\n```go\n// BAD: wastes an API call on every invalid input\nresult, _ := sms.Send(userInput, message, \"\")\n\n// GOOD: validate locally, only hit API with clean input\nv := kwtsms.ValidatePhoneInput(userInput)\nif !v.Valid {\n    return fmt.Errorf(\"invalid phone: %s\", v.Error)\n}\ncleaned := kwtsms.CleanMessage(message)\nif strings.TrimSpace(cleaned) == \"\" {\n    return fmt.Errorf(\"message is empty after cleaning\")\n}\nresult, _ := sms.Send(v.Normalized, message, \"\")\n```\n\nThe `Send()` method does validate and clean internally, but checking first lets you return errors to the user immediately without a network round-trip.\n\n### 2. Cache coverage at startup\n\nCall `Coverage()` once at application startup and cache the active country prefixes. Before every send, check the number's country prefix against the cached list. If the country is not active, return an error immediately without hitting the API:\n\n```go\ncoverage := sms.Coverage()\n// Cache the active country prefixes, check before every send\n// \"SMS delivery to [country] is not available on this account.\"\n```\n\n### 3. Save balance-after and msg-id\n\n```go\nif result.Result == \"OK\" {\n    db.SaveBalance(result.BalanceAfter)   // track balance without extra API calls\n    db.SaveMsgID(result.MsgID)            // needed for Status() and DLR() later\n}\n```\n\nSet up low-balance alerts (e.g., when balance drops below 50 credits). Before bulk sends, estimate credit cost (number of recipients x pages per message) and warn if balance is insufficient.\n\n### 4. Sender ID\n\n`KWT-SMS` is the shared test sender. It causes delays and is blocked on Virgin Kuwait. **Never use in production.** Register a private sender ID at kwtsms.com.\n\nFor OTP/authentication, you **must** use a Transactional sender ID. Promotional sender IDs are filtered by DND (Do Not Disturb) on Zain and Ooredoo, meaning OTP messages silently fail and credits are still deducted.\n\n### 5. OTP implementation\n\n- Always include the app/company name: `\"Your OTP for APPNAME is: 123456\"`\n- Resend timer: minimum 3-4 minutes (KNET standard is 4 minutes)\n- OTP expiry: 3-5 minutes\n- Generate a new code on resend, invalidate all previous codes\n- Send to one number per request (avoid ERR028 batch rejection)\n- Use a Transactional sender ID (not Promotional)\n\n### 6. Rate limiting\n\nWait at least **15 seconds** before sending to the same number again (ERR028). The entire request is rejected if any number in a batch triggers this, even if other numbers are fine.\n\n### 7. Monitoring and alerting\n\nSet up alerts for:\n- Failed sends: sudden increase in error responses\n- Balance depletion: rapid decrease or approaching zero\n- Error rate spikes: especially ERR003 (credentials), ERR010/ERR011 (balance), ERR028 (rate limit)\n- Queue buildup: messages stuck in kwtSMS queue (check via dashboard)\n\n### 8. Keep libraries updated\n\nMonitor for security patches and updates to the kwtSMS client library. Subscribe to kwtSMS announcements for API changes.\n\n### 9. Compliance\n\nStay informed about local telecom regulations regarding sender IDs, message content, and user consent. Promotional SMS may require opt-in consent from recipients. Different countries have different rules: check before enabling international coverage.\n\n## Timestamps\n\n`unix-timestamp` values in API responses are in **GMT+3 (Asia/Kuwait)** server time, not UTC. Convert when storing or displaying. Log timestamps written by the client are always UTC ISO-8601.\n\n## Security Checklist\n\nBefore going live:\n\n```\n[ ] Bot protection enabled (CAPTCHA for web apps)\n[ ] Rate limit per phone number (max 3-5/hour)\n[ ] Rate limit per IP address (max 10-20/hour)\n[ ] Rate limit per user/session if authenticated\n[ ] Monitoring/alerting on abuse patterns\n[ ] Admin notification on low balance\n[ ] Test mode OFF (KWTSMS_TEST_MODE=0)\n[ ] Private Sender ID registered (not KWT-SMS)\n[ ] Transactional Sender ID for OTP (not promotional)\n```\n\nWithout rate limiting, a bot can drain your entire SMS balance in minutes.\n\n## What's Handled Automatically\n\n- **Phone normalization**: `+`, `00`, spaces, dashes, dots, parentheses stripped. Arabic-Indic digits converted. Leading zeros removed.\n- **Duplicate phone removal**: If the same number appears multiple times (in different formats), it is sent only once.\n- **Message cleaning**: Emojis removed (surrogate-pair safe). Hidden control characters (BOM, zero-width spaces, directional marks) removed. HTML tags stripped. Arabic-Indic digits in message body converted to Latin.\n- **Batch splitting**: More than 200 numbers are automatically split into batches of 200 with 0.5s delay between batches.\n- **ERR013 retry**: Queue-full errors are automatically retried up to 3 times with exponential backoff (30s / 60s / 120s).\n- **Error enrichment**: Every API error response includes an `action` field with a developer-friendly fix hint.\n- **Credential masking**: Passwords are always masked as `***` in log files. Never exposed.\n- **Never throws**: All public methods return structured error objects. They never panic on API errors.\n\n## Examples\n\nSee the [examples/](examples/) directory for runnable code:\n\n| Example | Description |\n|---------|-------------|\n| [00-raw-api](examples/00-raw-api/) | Call every kwtSMS endpoint using only the Go standard library (no dependencies) |\n| [01-basic-usage](examples/01-basic-usage/) | Load credentials, verify, send SMS, print result |\n| [02-otp-flow](examples/02-otp-flow/) | Generate OTP, validate phone, send, save msg-id |\n| [03-bulk-sms](examples/03-bulk-sms/) | Send to multiple numbers with mixed formats |\n| [04-http-handler](examples/04-http-handler/) | HTTP endpoint for sending SMS with validation |\n| [05-error-handling](examples/05-error-handling/) | Handle all error types with user-facing messages |\n| [06-otp-production](examples/06-otp-production/) | Production OTP server: rate limiting, expiry, resend cooldown, user-facing errors |\n\n## Testing\n\n```bash\n# Unit + mocked API tests (no credentials needed)\ngo test -v ./...\n\n# With race detector\ngo test -race ./...\n\n# Integration tests (hits live API with test_mode=true, no credits consumed)\nGO_USERNAME=go_user GO_PASSWORD=go_pass go test -v -tags integration ./...\n```\n\n## JSONL Logging\n\nEvery API call is logged to `kwtsms.log` (configurable) as one JSON line. Passwords are always masked as `***`. Timestamps are UTC ISO-8601.\n\n```json\n{\"ts\":\"2026-03-06T12:00:00Z\",\"endpoint\":\"send\",\"request\":{\"username\":\"go_user\",\"password\":\"***\",\"sender\":\"MY-APP\",\"mobile\":\"96598765432\",\"message\":\"Hello\",\"test\":\"1\"},\"response\":{\"result\":\"OK\",\"msg-id\":\"abc123\"},\"ok\":true}\n```\n\nLogging never crashes the main flow. Disk errors are silently ignored.\n\n## FAQ\n\n**1. My message was sent successfully (result: OK) but the recipient didn't receive it. What happened?**\n\nCheck the **Sending Queue** at [kwtsms.com](https://www.kwtsms.com/login/). 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.\n\n**2. What is the difference between Test mode and Live mode?**\n\n**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 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.\n\n**3. What is a Sender ID and why should I not use \"KWT-SMS\" in production?**\n\nA **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.\n\n**4. I'm getting ERR003 \"Authentication error\". What's wrong?**\n\nYou are using the wrong credentials. The API requires your **API username and API password**, NOT your account mobile number. Log in to [kwtsms.com](https://www.kwtsms.com/login/), go to Account \u003e API settings, and check your API credentials. Also make sure you are using POST (not GET) and `Content-Type: application/json`.\n\n**5. Can I send to international numbers (outside Kuwait)?**\n\nInternational sending is **disabled by default** on kwtSMS accounts. [Log in to your kwtSMS account](https://www.kwtsms.com/login/) 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.\n\n## Help \u0026 Support\n\n- **[kwtSMS FAQ](https://www.kwtsms.com/faq/)**: Answers to common questions about credits, sender IDs, OTP, and delivery\n- **[kwtSMS Support](https://www.kwtsms.com/support.html)**: Open a support ticket or browse help articles\n- **[Contact kwtSMS](https://www.kwtsms.com/#contact)**: Reach the kwtSMS team directly for Sender ID registration and account issues\n- **[API Documentation (PDF)](https://www.kwtsms.com/doc/KwtSMS.com_API_Documentation_v41.pdf)**: kwtSMS REST API v4.1 full reference\n- **[Best Practices](https://www.kwtsms.com/articles/sms-api-implementation-best-practices.html)**: SMS API implementation best practices\n- **[Integration Test Checklist](https://www.kwtsms.com/articles/sms-api-integration-test-checklist.html)**: Pre-launch testing checklist\n- **[Sender ID Help](https://www.kwtsms.com/sender-id-help.html)**: Sender ID registration and guidelines\n- **[kwtSMS Dashboard](https://www.kwtsms.com/login/)**: Recharge credits, buy Sender IDs, view message logs, manage coverage\n- **[Other Integrations](https://www.kwtsms.com/integrations.html)**: Plugins and integrations for other platforms and languages\n- **[Library Issues](https://github.com/boxlinknet/kwtsms-go/issues)**: Report bugs or request features for this Go client\n\n## License\n\nMIT. See [LICENSE](LICENSE).\n\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fboxlinknet%2Fkwtsms-go","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fboxlinknet%2Fkwtsms-go","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fboxlinknet%2Fkwtsms-go/lists"}