KwtSMS 0.3.0
dotnet add package KwtSMS --version 0.3.0
NuGet\Install-Package KwtSMS -Version 0.3.0
<PackageReference Include="KwtSMS" Version="0.3.0" />
<PackageVersion Include="KwtSMS" Version="0.3.0" />
<PackageReference Include="KwtSMS" />
paket add KwtSMS --version 0.3.0
#r "nuget: KwtSMS, 0.3.0"
#:package KwtSMS@0.3.0
#addin nuget:?package=KwtSMS&version=0.3.0
#tool nuget:?package=KwtSMS&version=0.3.0
kwtSMS C# Client
C# client for the kwtSMS API. Send SMS, check balance, validate numbers, list sender IDs, check coverage, get delivery reports. Zero dependencies on .NET 6+.
About kwtSMS
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
Prerequisites
You need the .NET SDK installed.
Step 1: Check if .NET is installed
dotnet --version
If you see a version number (6.0+), you're ready. If not, install .NET:
- All platforms: Download from https://dotnet.microsoft.com/download
- macOS (Homebrew):
brew install dotnet - Ubuntu/Debian:
sudo apt install dotnet-sdk-8.0 - Windows: Download the installer from the link above
Step 2: Create a project (if you don't have one)
dotnet new console -n my-project && cd my-project
Step 3: Install KwtSMS
dotnet add package KwtSMS
Or add to your .csproj:
<PackageReference Include="KwtSMS" Version="0.3.0" />
Install
dotnet add package KwtSMS
Quick Start
using KwtSMS;
// Load credentials from environment variables or .env file
var sms = KwtSmsClient.FromEnv();
// Verify credentials and check balance
var (ok, balance, error) = sms.Verify();
Console.WriteLine($"Balance: {balance} credits");
// Send SMS
var result = sms.Send("96598765432", "Your OTP for MyApp is: 123456");
if (result.Result == "OK")
{
Console.WriteLine($"Sent! Message ID: {result.MsgId}");
Console.WriteLine($"Balance after: {result.BalanceAfter}");
}
Setup
Create a .env file in your project root:
KWTSMS_USERNAME=csharp_username
KWTSMS_PASSWORD=csharp_password
KWTSMS_SENDER_ID=YOUR-SENDERID # Use KWT-SMS for testing only
KWTSMS_TEST_MODE=1 # 1=test (safe), 0=live
KWTSMS_LOG_FILE=kwtsms.log # or "" to disable
Add .env to your .gitignore:
.env
Or pass credentials directly:
var sms = new KwtSmsClient("csharp_username", "csharp_password", "YOUR-SENDERID", testMode: true);
API Methods
Verify Credentials
var (ok, balance, error) = sms.Verify();
// ok: true if credentials are valid
// balance: available credits
// error: error message if failed
Send SMS
// Single number
var result = sms.Send("96598765432", "Hello from C#");
// Multiple numbers (comma-separated)
var result = sms.Send("96598765432,96512345678", "Bulk message");
// Multiple numbers (array)
var result = sms.Send(new[] { "96598765432", "96512345678" }, "Bulk message");
// Override sender ID
var result = sms.Send("96598765432", "Hello", sender: "MY-APP");
The Send method automatically:
- Normalizes phone numbers (strips
+,00, spaces, dashes, converts Arabic digits) - Deduplicates numbers before sending
- Cleans message text (strips emojis, HTML, hidden characters, converts Arabic digits)
- Validates numbers locally before calling the API
- For >200 numbers: auto-batches with 0.5s delay, retries ERR013 with exponential backoff
Send with Retry (ERR028)
// Automatically retries on ERR028 (rate limit), sleeping 16 seconds between attempts
var result = sms.SendWithRetry("96598765432", "OTP: 123456", maxRetries: 3);
Check Balance
double? balance = sms.Balance();
// Returns cached value on API failure
Validate Numbers
var result = sms.Validate("96598765432", "+96512345678", "invalid");
// result.Ok = valid and routable numbers
// result.Er = numbers with format errors
// result.Nr = numbers with no route (country not activated)
// result.Rejected = numbers that failed local validation
List Sender IDs
var result = sms.SenderIds();
// result.SenderIds = ["KWT-SMS", "MY-APP", ...]
Coverage (Active Countries)
var result = sms.Coverage();
// result.Prefixes = ["965", "966", "971", ...]
Message Status
var result = sms.Status("msg-id-from-send-response");
// result.Status = "sent"
// result.Description = "Message successfully sent"
Delivery Report (International Only)
// DLR is only available for international (non-Kuwait) numbers.
// Wait at least 5 minutes after sending before checking.
var result = sms.Dlr("msg-id-from-send-response");
// result.Report = [{ Number: "971504496677", Status: "Received by recipient" }]
Utility Functions
using KwtSMS;
// Normalize phone number (digits only, no leading zeros)
string normalized = PhoneUtils.NormalizePhone("+965 9876-5432");
// "96598765432"
// Validate phone number
var validation = PhoneUtils.ValidatePhoneInput("user@email.com");
// validation.IsValid = false
// validation.Error = "'user@email.com' is an email address, not a phone number"
// Clean message text
string cleaned = MessageUtils.CleanMessage("Hello \U0001F600 World <b>bold</b>");
// "Hello World bold"
// Access all error codes
foreach (var kvp in ApiErrors.Errors)
Console.WriteLine($"{kvp.Key}: {kvp.Value}");
Phone Number Formats
All 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 |
\u0669\u0666\u0665\u0669\u0668\u0667\u0666\u0665\u0664\u0663\u0662 (Arabic-Indic) |
96598765432 |
Yes |
\u06F9\u06F6\u06F5\u06F9\u06F8\u06F7\u06F6\u06F5\u06F4\u06F3\u06F2 (Extended Arabic-Indic) |
96598765432 |
Yes |
123456 (too short) |
rejected | No |
user@gmail.com |
rejected | No |
Input Sanitization
MessageUtils.CleanMessage() is called automatically by Send() before every API call. It prevents the #1 cause of "message sent but not received" support tickets:
| Content | Effect without cleaning | What CleanMessage() does |
|---|---|---|
| Emojis | Stuck in queue, credits wasted, no error | Stripped |
| Hidden control characters (BOM, zero-width space, soft hyphen) | Spam filter rejection or queue stuck | Stripped |
| Arabic/Hindi numerals in body | OTP codes render inconsistently | Converted to Latin digits |
| HTML tags | ERR027, message rejected | Stripped |
| Directional marks (LTR, RTL) | May cause display issues | Stripped |
Arabic letters and Arabic text are fully supported and never stripped.
Error Handling
Every error response includes a developer-friendly Action field:
var result = sms.Send("96598765432", "Test");
if (result.Result == "ERROR")
{
Console.WriteLine($"Code: {result.Code}"); // "ERR003"
Console.WriteLine($"Description: {result.Description}"); // "Authentication error..."
Console.WriteLine($"Action: {result.Action}"); // "Wrong API username or password..."
}
The library never throws exceptions. All methods return structured results.
Cached Balance
The client caches balance from Verify() and Send() responses:
sms.Verify();
Console.WriteLine($"Balance: {sms.CachedBalance}");
Console.WriteLine($"Purchased: {sms.CachedPurchased}");
Never call Balance() after Send(). The send response already includes your updated balance in BalanceAfter. Save it to your database.
ASP.NET Core Integration
Register KwtSmsClient as a singleton (thread-safe, reuses HttpClient):
builder.Services.AddSingleton<KwtSmsClient>(sp => KwtSmsClient.FromEnv());
See examples/AspNetEndpoint for a complete endpoint example.
Credential Management
API credentials must NEVER be hardcoded. Recommended approaches:
Environment variables / .env file (default): Use
KwtSmsClient.FromEnv(). The.envfile is gitignored and editable without redeployment.Admin settings UI (web apps): Provide a settings page where an admin can update credentials and test the connection with
Verify().Secrets manager (production): Load from Azure Key Vault, AWS Secrets Manager, or HashiCorp Vault. Pass to the constructor.
Constructor injection (custom config): Pass credentials directly from your DI container.
// Azure Key Vault example
var username = await secretClient.GetSecretAsync("kwtsms-username");
var password = await secretClient.GetSecretAsync("kwtsms-password");
var sms = new KwtSmsClient(username.Value.Value, password.Value.Value, "MY-APP");
Server Timezone
The unix-timestamp values in API responses are GMT+3 (Asia/Kuwait server time), NOT UTC. Convert when storing or displaying:
var serverTime = DateTimeOffset.FromUnixTimeSeconds(result.UnixTimestamp!.Value);
// This timestamp is in GMT+3. Adjust if you need UTC:
var utcTime = serverTime.AddHours(-3);
Best Practices
Validate Before Calling the API
// Validate phone locally first
var validation = PhoneUtils.ValidatePhoneInput(userInput);
if (!validation.IsValid)
{
return new { error = validation.Error };
}
// Clean message
var message = MessageUtils.CleanMessage(userMessage);
if (string.IsNullOrWhiteSpace(message))
{
return new { error = "Message is empty after cleaning." };
}
// Only valid, clean input reaches the API
var result = sms.Send(validation.Normalized, message);
User-Facing Error Messages
Never expose raw API errors to end users:
| Situation | API Error | User Message |
|---|---|---|
| Invalid phone | ERR006, ERR025 | "Please enter a valid phone number in international format." |
| Wrong credentials | ERR003 | "SMS service is temporarily unavailable." (log + alert admin) |
| No balance | ERR010, ERR011 | "SMS service is temporarily unavailable." (alert admin to top up) |
| Rate limited | ERR028 | "Please wait a moment before requesting another code." |
| Message rejected | ERR031, ERR032 | "Your message could not be sent. Please try with different content." |
OTP Requirements
- Always include app name:
"Your OTP for APPNAME is: 123456" - Use Transactional Sender ID (not Promotional, not
KWT-SMS) - Minimum 3-4 minute resend timer (KNET standard: 4 minutes)
- OTP expiry: 3-5 minutes
- Generate a new code on every resend, invalidate previous codes
- Send to a single number per request (never batch OTP sends)
Sender ID
| Promotional | Transactional | |
|---|---|---|
| Use for | Bulk SMS, marketing, offers | OTP, alerts, notifications |
| DND delivery | Blocked (credits lost) | Bypasses DND |
| Speed | May have delays | Priority delivery |
| Cost | 10 KD one-time | 15 KD one-time |
KWT-SMS is a shared test sender: delays, blocked on Virgin Kuwait. Never use in production. Register your own at kwtsms.com. Sender ID is case-sensitive.
Security Checklist
BEFORE GOING LIVE:
[ ] Bot protection enabled (CAPTCHA for web, Device Attestation for mobile)
[ ] 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)
CLI
Build and run the CLI from the repository:
dotnet run --project tools/KwtSMS.Cli -- setup
dotnet run --project tools/KwtSMS.Cli -- verify
dotnet run --project tools/KwtSMS.Cli -- balance
dotnet run --project tools/KwtSMS.Cli -- send 96598765432 "Your OTP is: 123456"
dotnet run --project tools/KwtSMS.Cli -- send "96598765432,96512345678" "Hello!" --sender "MY APP"
dotnet run --project tools/KwtSMS.Cli -- validate 96598765432 +96512345678
dotnet run --project tools/KwtSMS.Cli -- senderid
dotnet run --project tools/KwtSMS.Cli -- coverage
dotnet run --project tools/KwtSMS.Cli -- status <msg-id>
dotnet run --project tools/KwtSMS.Cli -- dlr <msg-id>
Commands:
kwtsms setup # interactive .env wizard
kwtsms verify # test credentials, show balance
kwtsms balance # show available + purchased credits
kwtsms senderid # list sender IDs
kwtsms coverage # list active prefixes
kwtsms send <mobile> <message> [--sender ID] # send SMS
kwtsms validate <number> [number ...] # validate numbers
kwtsms status <msg-id> # check status
kwtsms dlr <msg-id> # delivery report
Test mode prints a visible warning before sending. Errors print action guidance.
Compatibility
| Target | Dependencies |
|---|---|
| .NET 8.0+ | Zero |
| .NET 6.0+ | Zero |
| .NET Standard 2.0 (.NET Framework 4.6.1+) | System.Text.Json |
Examples
| Example | Description |
|---|---|
| RawApi | Call every endpoint directly with HttpClient (no library needed) |
| BasicUsage | Connect, verify, send SMS |
| OtpFlow | OTP with proper error handling |
| BulkSms | Auto-batching for >200 numbers |
| AspNetEndpoint | ASP.NET Core minimal API |
| ErrorHandling | Error codes, validation, cleaning |
| OtpProduction | Production OTP with rate limiting |
Testing
# Unit + mock tests (no credentials needed)
dotnet test --filter "Category!=Integration"
# Integration tests (real API, test mode, no credits consumed)
export CSHARP_USERNAME=csharp_username
export CSHARP_PASSWORD=csharp_password
dotnet test --filter "Category=Integration"
What's Handled Automatically
- Phone normalization:
+,00, spaces, dashes, dots, parentheses stripped. Arabic-Indic digits converted. Leading zeros removed. - Duplicate phone removal: If the same number appears multiple times (in different formats), it is sent only once.
- 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.
- Batch splitting: More than 200 numbers are automatically split into batches of 200 with 0.5s delay between batches.
- ERR013 retry: Queue-full errors are automatically retried up to 3 times with exponential backoff (30s / 60s / 120s).
- Error enrichment: Every API error response includes an
Actionfield with a developer-friendly fix hint. - Credential masking: Passwords are always masked as
***in log files. Never exposed. - No exceptions: All public methods return structured results. They never throw on API errors.
FAQ
1. 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.
Help & Support
- 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
- Implementation Best Practices: Official guide for production-ready SMS integrations
- Integration Test Checklist: Pre-launch testing checklist from kwtSMS
- Sender ID Help: How to register, whitelist, and troubleshoot sender IDs
- kwtSMS Dashboard: Recharge credits, buy Sender IDs, view message logs, manage coverage
- Other Integrations: Plugins and integrations for other platforms and languages
License
MIT
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net5.0 was computed. net5.0-windows was computed. net6.0 is compatible. net6.0-android was computed. net6.0-ios was computed. net6.0-maccatalyst was computed. net6.0-macos was computed. net6.0-tvos was computed. net6.0-windows was computed. net7.0 was computed. net7.0-android was computed. net7.0-ios was computed. net7.0-maccatalyst was computed. net7.0-macos was computed. net7.0-tvos was computed. net7.0-windows was computed. net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 was computed. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
| .NET Core | netcoreapp2.0 was computed. netcoreapp2.1 was computed. netcoreapp2.2 was computed. netcoreapp3.0 was computed. netcoreapp3.1 was computed. |
| .NET Standard | netstandard2.0 is compatible. netstandard2.1 was computed. |
| .NET Framework | net461 was computed. net462 was computed. net463 was computed. net47 was computed. net471 was computed. net472 was computed. net48 was computed. net481 was computed. |
| MonoAndroid | monoandroid was computed. |
| MonoMac | monomac was computed. |
| MonoTouch | monotouch was computed. |
| Tizen | tizen40 was computed. tizen60 was computed. |
| Xamarin.iOS | xamarinios was computed. |
| Xamarin.Mac | xamarinmac was computed. |
| Xamarin.TVOS | xamarintvos was computed. |
| Xamarin.WatchOS | xamarinwatchos was computed. |
-
.NETStandard 2.0
- System.Text.Json (>= 10.0.3)
-
net6.0
- No dependencies.
-
net8.0
- No dependencies.
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.