{"id":49888342,"url":"https://github.com/boxlinknet/kwtsms-dart","last_synced_at":"2026-05-15T19:33:59.116Z","repository":{"id":342448856,"uuid":"1173963015","full_name":"boxlinknet/kwtsms-dart","owner":"boxlinknet","description":"Official Dart/Flutter client for the kwtSMS SMS gateway API (kwtsms.com). Zero dependencies.","archived":false,"fork":false,"pushed_at":"2026-03-13T20:31:03.000Z","size":178,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-03-14T07:46:38.161Z","etag":null,"topics":["dart","flutter","kuwait","kwtsms","otp","sms","sms-api","sms-gateway"],"latest_commit_sha":null,"homepage":"https://www.kwtsms.com/developers.html","language":"Dart","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:05.000Z","updated_at":"2026-03-13T20:30:59.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/boxlinknet/kwtsms-dart","commit_stats":null,"previous_names":["boxlinknet/kwtsms-dart"],"tags_count":9,"template":false,"template_full_name":null,"purl":"pkg:github/boxlinknet/kwtsms-dart","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/boxlinknet%2Fkwtsms-dart","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/boxlinknet%2Fkwtsms-dart/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/boxlinknet%2Fkwtsms-dart/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/boxlinknet%2Fkwtsms-dart/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/boxlinknet","download_url":"https://codeload.github.com/boxlinknet/kwtsms-dart/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/boxlinknet%2Fkwtsms-dart/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33076226,"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":["dart","flutter","kuwait","kwtsms","otp","sms","sms-api","sms-gateway"],"created_at":"2026-05-15T19:33:58.325Z","updated_at":"2026-05-15T19:33:59.108Z","avatar_url":"https://github.com/boxlinknet.png","language":"Dart","funding_links":[],"categories":[],"sub_categories":[],"readme":"# kwtSMS Dart Client\n\n[![pub package](https://img.shields.io/pub/v/kwtsms.svg)](https://pub.dev/packages/kwtsms)\n[![CI](https://github.com/boxlinknet/kwtsms-dart/actions/workflows/test.yml/badge.svg)](https://github.com/boxlinknet/kwtsms-dart/actions/workflows/test.yml)\n[![Static Analysis](https://github.com/boxlinknet/kwtsms-dart/actions/workflows/codeql.yml/badge.svg)](https://github.com/boxlinknet/kwtsms-dart/actions/workflows/codeql.yml)\n[![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](https://opensource.org/licenses/MIT)\n[![Dart 3](https://img.shields.io/badge/dart-%3E%3D3.0-blue.svg)](https://dart.dev)\n[![pub points](https://img.shields.io/pub/points/kwtsms)](https://pub.dev/packages/kwtsms/score)\n\nDart/Flutter client for the [kwtSMS API](https://www.kwtsms.com). Send SMS, check balance, validate numbers, list sender IDs, check coverage, get delivery reports.\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 **Dart** (3.0 or newer) installed. If you use Flutter, Dart is included. Zero runtime dependencies.\n\n### Option A: Dart only (server-side)\n\n```bash\ndart --version\n```\n\nIf you see a version number (e.g., `Dart SDK version: 3.x.x`), Dart is installed. If not:\n\n- **macOS:** `brew tap dart-lang/dart \u0026\u0026 brew install dart`\n- **Ubuntu/Debian:** Follow instructions at https://dart.dev/get-dart#install\n- **Windows:** Download from https://dart.dev/get-dart#install\n\n### Option B: Flutter (mobile / cross-platform apps)\n\n```bash\nflutter --version\n```\n\nIf you see a version number, Flutter (with Dart) is installed. If not, follow https://docs.flutter.dev/get-started/install\n\n### Install kwtsms\n\nFor a Dart project:\n```bash\ndart pub add kwtsms\n```\n\nFor a Flutter project:\n```bash\nflutter pub add kwtsms\n```\n\n## Quick Start\n\n```dart\nimport 'package:kwtsms/kwtsms.dart';\n\nvoid main() async {\n  final sms = KwtSMS('dart_username', 'dart_password');\n\n  // Verify credentials\n  final verify = await sms.verify();\n  print('Balance: ${verify.balance}');\n\n  // Send SMS\n  final result = await sms.send('96598765432', 'Hello from Dart!');\n  print('Result: ${result.result}, Message ID: ${result.msgId}');\n}\n```\n\n## Setup / Configuration\n\n### Option 1: Environment variables\n\n```bash\nexport KWTSMS_USERNAME=dart_username\nexport KWTSMS_PASSWORD=dart_password\nexport KWTSMS_SENDER_ID=YOUR-SENDERID\nexport KWTSMS_TEST_MODE=0\nexport KWTSMS_LOG_FILE=kwtsms.log\n```\n\n```dart\nfinal sms = KwtSMS.fromEnv();\n```\n\n### Option 2: .env file\n\nCreate a `.env` file in your project root:\n\n```ini\nKWTSMS_USERNAME=dart_username\nKWTSMS_PASSWORD=dart_password\nKWTSMS_SENDER_ID=YOUR-SENDERID\nKWTSMS_TEST_MODE=1\nKWTSMS_LOG_FILE=kwtsms.log\n```\n\n```dart\nfinal sms = KwtSMS.fromEnv(); // reads .env automatically\n```\n\n### Option 3: Direct constructor\n\n```dart\nfinal sms = KwtSMS(\n  'dart_username',\n  'dart_password',\n  senderId: 'YOUR-SENDERID',\n  testMode: true,\n  logFile: 'kwtsms.log',\n);\n```\n\n## Credential Management\n\n**Never hardcode credentials.** Use one of these approaches:\n\n1. **Environment variables / .env file** (default): `KwtSMS.fromEnv()` loads from env vars, then `.env` file. The file is `.gitignore`d and editable without redeployment.\n\n2. **Constructor injection**: `KwtSMS(username, password, ...)` for custom config systems, DI containers, or remote config.\n\n3. **Secrets manager**: Load from AWS Secrets Manager, HashiCorp Vault, Google Secret Manager, or your own config API, then pass to the constructor.\n\n4. **Admin settings UI** (for web apps): Store credentials in your database with a settings page. Include a \"Test Connection\" button that calls `verify()`.\n\n### Additional requirements for mobile apps (Flutter)\n\n**Backend proxy (strongly recommended):** The mobile app calls YOUR backend server, which holds the kwtSMS credentials and makes the API call. The app never touches the SMS API directly. This is the only pattern that fully protects credentials.\n\n**If calling the API directly (not recommended):** Store credentials using `flutter_secure_storage` and provide a settings screen for entering/updating them. NEVER store credentials in assets, hardcoded strings, or environment files bundled with the app.\n\n### Thread Safety\n\nDart is single-threaded (event loop). No mutex is needed for cached balance in standard use. If using Isolates, create a separate `KwtSMS` instance per Isolate.\n\n## All Methods\n\n### verify()\n\nTest credentials and get the current balance.\n\n```dart\nfinal result = await sms.verify();\n// VerifyResult(ok: true, balance: 150.0, error: null)\n\nif (result.ok) {\n  print('Balance: ${result.balance}');\n} else {\n  print('Error: ${result.error}');\n}\n```\n\n### balance()\n\nGet the current SMS credit balance. Returns cached value if the API call fails.\n\n```dart\nfinal bal = await sms.balance();\nprint('Balance: $bal');\n\n// Cached values (updated after verify/send):\nprint('Cached balance: ${sms.cachedBalance}');\nprint('Purchased: ${sms.cachedPurchased}');\n```\n\n### send(mobile, message, {sender})\n\nSend SMS to one or more phone numbers. Auto-batches when \u003e200 numbers.\n\n```dart\n// Single number\nfinal result = await sms.send('96598765432', 'Your OTP is: 123456');\n\nif (result.result == 'OK') {\n  print('Message ID: ${result.msgId}');         // save this!\n  print('Points charged: ${result.pointsCharged}');\n  print('Balance after: ${result.balanceAfter}'); // save this too!\n}\n\n// Multiple numbers (comma-separated)\nfinal result2 = await sms.send(\n  '96598765432,96512345678',\n  'Hello everyone!',\n);\n\n// Override sender ID\nfinal result3 = await sms.send(\n  '96598765432',\n  'Alert!',\n  sender: 'MY-APP',\n);\n```\n\n**What happens automatically:**\n- Phone numbers are normalized (strips +, 00, spaces, dashes, converts Arabic digits ٠١٢٣٤٥٦٧٨٩)\n- Numbers are deduplicated after normalization\n- Invalid numbers are collected in `result.invalid` without crashing\n- Message text is cleaned (emojis stripped, HTML removed, control chars removed)\n- For \u003e200 numbers, splits into batches of 200 with 0.5s delay\n\n### sendBulk(mobiles, message, {sender})\n\nExplicitly send to a list of numbers in batches.\n\n```dart\nfinal numbers = ['96598765432', '96512345678', /* ... hundreds more */];\nfinal result = await sms.sendBulk(numbers, 'Campaign message');\n\n// BulkSendResult\nprint('Result: ${result.result}');     // OK, PARTIAL, or ERROR\nprint('Batches: ${result.batches}');\nprint('Numbers sent: ${result.numbers}');\nprint('Points charged: ${result.pointsCharged}');\nprint('Message IDs: ${result.msgIds}');\n\nif (result.errors.isNotEmpty) {\n  for (final err in result.errors) {\n    print('Batch ${err.batch}: ${err.code} - ${err.description}');\n  }\n}\n```\n\n### validate(phones)\n\nValidate phone numbers with the kwtSMS API.\n\n```dart\nfinal result = await sms.validate(['96598765432', '123', 'test@email.com']);\n\nprint('Valid (OK): ${result.ok}');        // routable numbers\nprint('Format error (ER): ${result.er}'); // format issues\nprint('No route (NR): ${result.nr}');     // country not activated\n\n// Numbers that failed local validation (never sent to API):\nfor (final entry in result.rejected) {\n  print('${entry.input}: ${entry.error}');\n}\n```\n\n### senderIds()\n\nList available sender IDs on this account.\n\n```dart\nfinal result = await sms.senderIds();\nif (result.result == 'OK') {\n  for (final id in result.senderIds) {\n    print('Sender ID: $id');\n  }\n}\n```\n\n### coverage()\n\nList active country prefixes for SMS delivery.\n\n```dart\nfinal result = await sms.coverage();\nif (result.result == 'OK') {\n  for (final prefix in result.prefixes) {\n    print('Active prefix: $prefix');\n  }\n}\n```\n\n### status(msgId)\n\nCheck delivery status of a sent message.\n\n```dart\nfinal result = await sms.status('f4c841adee210f31307633ceaebff2ec');\nif (result.result == 'OK') {\n  print('Status: ${result.status}');\n  print('Description: ${result.statusDescription}');\n}\n```\n\n### deliveryReport(msgId)\n\nGet per-number delivery reports (international numbers only, Kuwait numbers do not support DLR).\n\n```dart\nfinal result = await sms.deliveryReport('f4c841adee210f31307633ceaebff2ec');\nif (result.result == 'OK') {\n  for (final entry in result.report) {\n    print('${entry.number}: ${entry.status}');\n  }\n}\n```\n\n## Utility Functions\n\nThese are exported publicly and can be used independently.\n\n### normalizePhone(phone)\n\nStrips non-digits, converts Arabic/Persian digits to Latin, removes leading zeros, and strips domestic trunk prefixes after the country code.\n\n```dart\nnormalizePhone('+96598765432');     // '96598765432'\nnormalizePhone('0096598765432');    // '96598765432'\nnormalizePhone('965 9876 5432');    // '96598765432'\nnormalizePhone('965-9876-5432');    // '96598765432'\nnormalizePhone('٩٦٥٩٨٧٦٥٤٣٢');         // '96598765432' (Arabic digits converted)\n\n// Trunk prefix stripping: removes the leading 0 after the country code\nnormalizePhone('9660559876543');    // '966559876543' (Saudi 0 stripped)\nnormalizePhone('+9660559876543');   // '966559876543'\nnormalizePhone('9710501234567');    // '971501234567' (UAE 0 stripped)\nnormalizePhone('20010123456789');   // '2010123456789' (Egypt 0 stripped)\n```\n\n### validatePhoneInput(phone)\n\nValidates a phone number input. Runs normalization, basic checks (empty, email, length), and country-specific format validation (local length + mobile prefix).\n\n```dart\nfinal (valid, error, normalized) = validatePhoneInput('96598765432');\n// (true, null, '96598765432')\n\nfinal (v2, e2, n2) = validatePhoneInput('123');\n// (false, \"'123' is too short (3 digits, minimum is 7)\", '123')\n\nfinal (v3, e3, n3) = validatePhoneInput('user@example.com');\n// (false, \"'user@example.com' is an email address, not a phone number\", '')\n\n// Country-specific validation\nfinal (v4, e4, n4) = validatePhoneInput('96512345678');\n// (false, \"Invalid Kuwait mobile number: after +965 must start with 4, 5, 6, 9\", '96512345678')\n\nfinal (v5, e5, n5) = validatePhoneInput('9651234567');\n// (false, \"Invalid Kuwait number: expected 8 digits after +965, got 7\", '9651234567')\n```\n\n### findCountryCode(normalized)\n\nFinds the country code prefix from a normalized phone number. Tries 3-digit, then 2-digit, then 1-digit (longest match wins).\n\n```dart\nfindCountryCode('96598765432');   // '965' (Kuwait)\nfindCountryCode('201012345678');  // '20'  (Egypt)\nfindCountryCode('12025551234');   // '1'   (USA/Canada)\nfindCountryCode('9991234567');    // null  (unknown)\n```\n\n### validatePhoneFormat(normalized)\n\nValidates a normalized phone number against country-specific rules: local number length and mobile starting digits. Numbers with no matching country rules pass through.\n\n```dart\nfinal (valid, error) = validatePhoneFormat('96598765432');\n// (true, null)\n\nfinal (v2, e2) = validatePhoneFormat('96512345678');\n// (false, \"Invalid Kuwait mobile number: after +965 must start with 4, 5, 6, 9\")\n```\n\n### phoneRules\n\nMap of 80+ countries with validation rules. Each entry has `localLengths` (valid digit counts after country code) and `mobileStartDigits` (valid first digits for mobile numbers).\n\n```dart\nphoneRules['965']; // PhoneRule(localLengths: [8], mobileStartDigits: ['4','5','6','9'])\nphoneRules['966']; // PhoneRule(localLengths: [9], mobileStartDigits: ['5'])\nphoneRules['1'];   // PhoneRule(localLengths: [10]) -- USA/Canada, no prefix check\n```\n\n### countryNames\n\nMap of country codes to human-readable country names. Used in error messages.\n\n```dart\ncountryNames['965']; // 'Kuwait'\ncountryNames['966']; // 'Saudi Arabia'\ncountryNames['44'];  // 'UK'\n```\n\n### deduplicatePhones(phones)\n\nRemoves duplicate phone numbers from a list while preserving order.\n\n```dart\ndeduplicatePhones(['96598765432', '96512345678', '96598765432']);\n// ['96598765432', '96512345678']\n```\n\n### cleanMessage(text)\n\n```dart\ncleanMessage('Hello \\u{1F600}');  // 'Hello ' (emoji stripped)\ncleanMessage('\u003cb\u003eBold\u003c/b\u003e');      // 'Bold' (HTML stripped)\ncleanMessage('\\uFEFFHello');      // 'Hello' (BOM stripped)\ncleanMessage('رمز التحقق: ١٢٣٤٥٦'); // 'رمز التحقق: 123456' (Arabic digits converted)\n```\n\n### enrichError(response)\n\n```dart\nfinal enriched = enrichError({\n  'result': 'ERROR',\n  'code': 'ERR003',\n  'description': 'Authentication error',\n});\nprint(enriched['action']); // Developer-friendly guidance\n```\n\n### apiErrors\n\nRead-only map of all 33 error codes to action messages. Useful for building custom error UIs.\n\n```dart\nprint(apiErrors['ERR003']); // 'Wrong API username or password...'\n```\n\n### loadEnvFile([path])\n\n```dart\nfinal vars = loadEnvFile('.env');\nprint(vars['KWTSMS_USERNAME']);\n```\n\n## Input Sanitization\n\n`cleanMessage()` is called automatically by `send()` before every API call. It prevents the #1 cause of \"message sent but not received\" support tickets:\n\n| Content | Effect without cleaning | What cleanMessage() does |\n|---------|------------------------|--------------------------|\n| Emojis | Stuck in queue, credits wasted, no error | Stripped |\n| Hidden control characters (BOM, zero-width space, soft hyphen) | Spam filter rejection or queue stuck | Stripped |\n| Arabic/Hindi numerals in body | OTP codes render inconsistently | Converted to Latin digits |\n| HTML tags | ERR027, message rejected | Stripped |\n| Directional marks (LTR, RTL) | May cause display issues | Stripped |\n\nArabic letters and Arabic text are fully supported and never stripped.\n\n## Error Handling\n\nEvery ERROR response includes an `action` field with a developer-friendly fix:\n\n```dart\nfinal result = await sms.send('96598765432', 'Hello');\n\nif (result.result == 'OK') {\n  // Save msg-id and balance-after\n  db.save('sms_balance', result.balanceAfter);\n  db.save('msg_id', result.msgId);\n} else {\n  // Error handling\n  print('Code: ${result.code}');\n  print('Description: ${result.description}');\n  print('Action: ${result.action}'); // developer-friendly guidance\n}\n\n// Invalid numbers are collected, never crash the call\nfor (final entry in result.invalid) {\n  print('${entry.input}: ${entry.error}');\n}\n```\n\n### User-facing error mapping\n\nRaw API errors should never be shown to end users. Map them:\n\n| Situation | API error | Show to user |\n|-----------|----------|--------------|\n| Invalid phone number | 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.\" (log + alert admin) |\n| No balance | ERR010, ERR011 | \"SMS service is temporarily unavailable. Please try again later.\" (alert admin) |\n| Country not supported | ERR026 | \"SMS delivery to this country is not available.\" |\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| Queue full | ERR013 | \"SMS service is busy. Please try again in a few minutes.\" (library retries automatically) |\n| Network error | Connection timeout | \"Could not connect to SMS service.\" |\n\n### Common Error Codes\n\n| Code | Meaning | Action |\n|------|---------|--------|\n| ERR003 | Wrong credentials | Check KWTSMS_USERNAME and KWTSMS_PASSWORD |\n| ERR006 | No valid phone numbers | Include country code (e.g., 96598765432) |\n| ERR008 | Sender ID banned | Use a different sender ID |\n| ERR009 | Empty message | Provide a non-empty message |\n| ERR010 | Zero balance | Recharge at kwtsms.com |\n| ERR011 | Insufficient balance | Buy more credits |\n| ERR013 | Queue full | Retried automatically (3x with backoff) |\n| ERR024 | IP not whitelisted | Add IP at kwtsms.com \u003e API \u003e IP Lockdown |\n| ERR025 | Invalid phone number | Include the country code |\n| ERR026 | Country not activated | Contact kwtSMS support |\n| ERR028 | 15-second rate limit | Wait before resending to same number |\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| `٩٦٥٩٨٧٦٥٤٣٢` | `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\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 this during development.\n\n**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```dart\nfinal sms = KwtSMS('user', 'pass', testMode: true);\nfinal result = await sms.send('96598765432', 'Test message');\n// Message is queued but NOT delivered. No credits consumed.\n```\n\nTest messages appear in the Sending Queue at kwtsms.com. Delete them from the queue to recover any tentatively held credits. Remember to set `testMode: false` before going live.\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| **Delivery to DND numbers** | Blocked/filtered, credits lost | Bypasses DND (whitelisted) |\n| **Speed** | May have delays | Priority delivery |\n| **Cost** | 10 KD one-time | 15 KD one-time |\n\n`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 filtering. Sender ID is **case sensitive**.\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.\n\n## Best Practices\n\n### Always save msg-id and balance-after\n\n```dart\nfinal result = await sms.send(phone, message);\nif (result.result == 'OK') {\n  db.save('sms_msg_id', result.msgId);         // needed for status/DLR\n  db.save('sms_balance', result.balanceAfter);  // no extra API call needed\n}\n```\n\n### Validate locally before calling the API\n\n```dart\nfinal (valid, error, normalized) = validatePhoneInput(userInput);\nif (!valid) {\n  return {'error': error};  // rejected locally, no API call\n}\n```\n\n### Country coverage pre-check\n\nCall `coverage()` once at startup and cache the active prefixes. Before every send, check if the number's country prefix is in the list. If not, return an error immediately without hitting the API.\n\n```dart\n// At startup\nfinal coverage = await sms.coverage();\nfinal activePrefixes = coverage.prefixes;\n\n// Before send\nif (!activePrefixes.any((p) =\u003e normalized.startsWith(p))) {\n  return {'error': 'SMS delivery to this country is not available.'};\n}\n```\n\n### OTP requirements\n\n- Always include 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- New code on resend: always generate a fresh code, invalidate previous\n- Use Transactional Sender ID for OTP (not Promotional, not KWT-SMS)\n- One number per OTP request: never batch OTP sends\n\n## Implementation Checklist\n\nBefore going live, verify you have implemented these correctly:\n\n- [ ] Validate phone numbers locally before calling the API (reject emails, too-short, too-long)\n- [ ] Clean message text before sending (emojis, HTML, hidden characters)\n- [ ] Check country coverage before sending (cache prefixes from `coverage()`)\n- [ ] Save `msg-id` from every successful send (needed for status/DLR)\n- [ ] Save `balance-after` from every successful send (never call `balance()` after `send()`)\n- [ ] Map raw API errors to user-facing messages (never expose ERR codes to users)\n- [ ] Log errors with full details for admin review\n- [ ] Set up low-balance alerts\n- [ ] Handle ERR028 (15-second same-number rate limit) in your UI\n- [ ] Use Transactional Sender ID for OTP (not Promotional)\n\n## Security Checklist\n\nBefore going live:\n\n- [ ] Bot protection enabled (Device Attestation for mobile, CAPTCHA for web)\n- [ ] Rate limit per phone number (max 3-5 OTP/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## 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 (codepoint-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- **Balance caching**: Balance is cached from every `verify()` and `send()` response. `balance()` falls back to the cached value on API failure.\n\n## Examples\n\nSee the [example/](example/) directory:\n\n| # | Example | Description |\n|---|---------|-------------|\n| 00 | [Raw API](example/00_raw_api.dart) | Call every kwtSMS endpoint directly, no library, just dart:io ([docs](example/00_raw_api.md)) |\n| 01 | [Basic Usage](example/01_basic_usage.dart) | Load credentials, verify, send SMS, print result |\n| 02 | [OTP Flow](example/02_otp_flow.dart) | Generate OTP, validate phone, send, save msg-id |\n| 03 | [Bulk SMS](example/03_bulk_sms.dart) | Send to multiple numbers with mixed formats |\n| 04 | [Shelf Endpoint](example/04_shelf_endpoint.dart) | Shelf HTTP endpoint for sending SMS with validation |\n| 05 | [Error Handling](example/05_error_handling.dart) | Handle all error types with user-facing messages |\n| 06 | [OTP Production](example/06_otp_production/) | Production OTP service: rate limiting, hashing, device attestation, resend cooldown |\n\n## CLI\n\nFor command-line usage, see [kwtsms-cli](https://github.com/boxlinknet/kwtsms-cli), a standalone cross-platform binary (no Dart SDK required).\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, 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**6. Can I use this with Flutter?**\n\nYes. The library works in both pure Dart (server-side) and Flutter (mobile) contexts. Install with `flutter pub add kwtsms`.\n\n**7. How do I check if a message was delivered?**\n\nSave the `msg-id` from the send response, then call `sms.status(msgId)`. For international numbers, use `sms.deliveryReport(msgId)` (wait 5+ minutes). Kuwait numbers do not support delivery reports.\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)**: Guide to registering and managing Sender IDs\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-dart/issues)**: Report bugs or request features for this Dart client\n\n## License\n\nMIT\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fboxlinknet%2Fkwtsms-dart","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fboxlinknet%2Fkwtsms-dart","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fboxlinknet%2Fkwtsms-dart/lists"}