A while back, I needed to integrate SMS into a .NET project. Giant SMS had a REST API, but no official .NET client. The only existing library was a PHP one from 6–7 years ago, and it only covered two methods: send and getBalance.
So I built my own. It now has nearly 2,000 downloads on NuGet. Here's exactly how I did it.
The Problem
Wiring up raw HTTP calls to the Giant SMS API in every project gets repetitive fast:
- Manually setting
Authorizationheaders - Remembering which endpoints use token auth vs. username/password
- Deserializing responses every time
- Scattering credentials across your codebase
I wanted something that felt native to .NET. Configure once in appsettings.json,
register with DI, and just call a method.
Designing the Public API
The first decision was the interface. I wanted consumers to never touch HttpClient
directly, and I wanted methods that mapped clearly to what the API actually does:
public interface IGiantSmsService
{
bool IsReady { get; }
Task<SingleSmsResponse> SendSingleMessage(string to, string msg);
Task<SingleSmsResponse> SendMessageWithToken(SingleMessageRequest messageRequest);
Task<BaseResponse> SendBulkMessages(BulkMessageRequest messageRequest);
Task<SingleSmsResponse> CheckMessageStatus(string messageId);
Task<BaseResponse> GetBalance();
Task<SenderIdResponse> GetSenderIds();
Task<BaseResponse> RegisterSenderId(RegisterSenderIdRequest senderIdRequest);
}
Seven methods, the full surface of the API, no more, no less.
The IsReady property is a small but useful addition. It lets consumers do a quick sanity check at startup rather than discovering a missing token on the first SMS send:
csharp _isReady = !string.IsNullOrWhiteSpace(_connection.Token) &&
!string.IsNullOrWhiteSpace(_connection.Username);
Handling Two Auth Methods
This was the most interesting design challenge. The Giant SMS API uses two different
authentication schemes depending on the endpoint:
Token-based (Basic Authorization header) — for sending messages and checking status
Username/password (query parameters) — for balance, sender IDs, and registration
The library handles both transparently. Consumers never have to think about which auth method an endpoint needs:
// Token auth — Authorization header
request.Headers.Add("Authorization", $"Basic {token}");
// Username/password auth — query string
await _httpClient.GetAsync(
$"{BaseEndpointUrl}/balance?username={username}&password={password}");
All credentials live in a single configuration object:
public class GiantSmsConnection
{
public string Username { get; set; }
public string Password { get; set; }
public string Token { get; set; }
public string SenderId { get; set; }
}
Configuration via appsettings.json
Add this to your appsettings.json:
"GiantSmsConnection": {
"Username": "your_username",
"Password": "your_password",
"Token": "your_token",
"SenderId": "YourApp"
}
The library binds to it automatically using the IOptions<T> pattern , no manual
mapping needed.
DI Registration in One Line
builder.Services.AddGiantSms(builder.Configuration);
Under the hood:
public static void AddGiantSms(this IServiceCollection services, IConfiguration configuration)
{
services.AddHttpClient();
services.Configure<GiantSmsConnection>(options =>
configuration.GetSection(nameof(GiantSmsConnection)).Bind(options));
services.AddScoped<IGiantSmsService, GiantSmsService>();
}
IHttpClientFactory is used instead of newing up HttpClient directly. This avoids socket exhaustion in long-running applications, a common footgun in .NET.
Then inject and use anywhere:
public class NotificationService
{
private readonly IGiantSmsService _sms;
public NotificationService(IGiantSmsService sms) => _sms = sms;
public async Task Notify(string phone, string message)
{
if (!_sms.IsReady) return;
await _sms.SendSingleMessage(phone, message);
}
}
Packaging for NuGet
The .csproj is where you define your package identity:
<PackageId>GiantSms.Net</PackageId>
<Version>2.0.0</Version>
<Authors>Benjamin Aduo</Authors>
<Description>A simple .NET library for sending SMS via the Giant SMS API.</Description>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<RepositoryUrl>https://github.com/benaduo/GiantSms.Net</RepositoryUrl>
A few things worth noting:
snupkg publishes debug symbols separately so consumers can step into your library during debugging
GeneratePackageOnBuild produces a .nupkg on every build, this is convenient during development
Always include a RepositoryUrl, it shows on the NuGet page and builds trust
Publishing is one command once you have an API key from nuget.org:
dotnet nuget push GiantSms.Net.2.0.0.nupkg --api-key YOUR_KEY --source https://api.nuget.org/v3/index.json
The Result
The package is live at nuget.org/packages/GiantSms.Net with nearly 2,000 downloads. Not millions but knowing real developers are using something you built from scratch is a good feeling.
dotnet add package GiantSms.Net
Full source on GitHub. PRs and issues welcome.
Top comments (0)