Hellio.Messaging
1.2.0
dotnet add package Hellio.Messaging --version 1.2.0
NuGet\Install-Package Hellio.Messaging -Version 1.2.0
<PackageReference Include="Hellio.Messaging" Version="1.2.0" />
<PackageVersion Include="Hellio.Messaging" Version="1.2.0" />
<PackageReference Include="Hellio.Messaging" />
paket add Hellio.Messaging --version 1.2.0
#r "nuget: Hellio.Messaging, 1.2.0"
#:package Hellio.Messaging@1.2.0
#addin nuget:?package=Hellio.Messaging&version=1.2.0
#tool nuget:?package=Hellio.Messaging&version=1.2.0
Hellio Messaging - Official .NET SDK
.NET client for the Hellio Messaging API v1: SMS, OTP (SMS / email / voice), Voice broadcasts, Number Lookup (HLR), Email Verification, USSD, and Webhooks.
Targets netstandard2.0 and net8.0, so it runs on .NET Framework 4.6.1+, .NET Core,
Xamarin, and modern .NET.
Install
dotnet add package Hellio.Messaging
Or with the Package Manager Console:
Install-Package Hellio.Messaging
Configure
Generate a token in your dashboard (Settings, then API, then Generate API token), then
construct the client. You can pass values directly or set the environment variables
HELLIO_API_TOKEN, HELLIO_BASE_URL, and HELLIO_DEFAULT_SENDER.
using Hellio.Messaging;
var hellio = new HellioClient(
token: "your-token-here",
baseUrl: "https://api.helliomessaging.com/v1", // optional, this is the default
defaultSender: "HellioSMS"); // optional Sender ID for SMS
Reading from the environment instead:
// Uses HELLIO_API_TOKEN, HELLIO_BASE_URL, HELLIO_DEFAULT_SENDER when arguments are omitted.
var hellio = new HellioClient();
Every call returns a System.Text.Json.JsonElement (payloads live under a data key),
except VerifyAsync, which returns a bool, and the Ussd methods, which return typed
models (see USSD). All methods are async and accept an optional CancellationToken.
Usage
using Hellio.Messaging;
using System.Text.Json;
var hellio = new HellioClient(token: "your-token-here", defaultSender: "HellioSMS");
// Account
JsonElement balance = await hellio.BalanceAsync(); // data.balance, data.available, ...
JsonElement pricing = await hellio.PricingAsync("GH"); // optional ISO-2 country filter
// SMS (recipients: single string, comma list, or IEnumerable<string>)
await hellio.SendSmsAsync("233241234567", "Hello!");
await hellio.SendSmsAsync(new[] { "233241234567", "233201234567" }, "Hi all", "HellioSMS");
await hellio.MessageAsync(1024); // delivery status
await hellio.CampaignAsync(1024); // campaign summary
// OTP - sender (Sender ID) is REQUIRED for sms/voice and must be approved on your account.
// Optional length (4 to 10 digits) and expiry (minutes). Returns status "queued".
await hellio.SendOtpAsync("233241234567", "HellioSMS"); // SMS
await hellio.SendOtpAsync("233241234567", "HellioSMS", channel: "voice"); // Voice (TTS reads the code)
await hellio.SendOtpAsync("233241234567", "HellioSMS", length: 6, expiry: 10); // custom length / expiry
await hellio.SendOtpAsync("user@example.com", channel: "email"); // Email (no sender)
bool ok = await hellio.VerifyAsync("233241234567", "123456"); // bool convenience
JsonElement res = await hellio.VerifyOtpAsync("user@example.com", "123456", "email"); // full response
// Voice broadcast - text (we TTS it) or a hosted audioUrl
await hellio.SendVoiceAsync("233241234567", "HELLIO", text: "Your code is 1 2 3 4");
await hellio.SendVoiceAsync(new[] { "233241234567" }, "HELLIO", audioUrl: "https://cdn.example.com/promo.mp3");
await hellio.VoiceStatusAsync(2048);
// Number lookup (HLR) - async; poll results
await hellio.LookupAsync(new[] { "233241234567" });
await hellio.LookupsAsync();
await hellio.LookupResultAsync(5);
// Email verification
await hellio.VerifyEmailAsync(new[] { "user@gmail.com", "bad@nodomain.invalid" });
// Webhooks (receive delivery reports)
await hellio.CreateWebhookAsync("https://your-app.com/hooks/hellio",
new[] { "message.delivered", "message.failed" });
await hellio.WebhooksAsync();
await hellio.DeleteWebhookAsync(1);
Reading responses
Responses are JsonElement, so you can navigate them directly:
JsonElement balance = await hellio.BalanceAsync();
string available = balance.GetProperty("data").GetProperty("available").GetString();
USSD
USSD lives under hellio.Ussd and needs a token with the ussd ability. Unlike the rest
of the SDK, these methods return typed models (for example UssdApp, UssdExtension,
UssdSession) rather than a raw JsonElement. List methods accept an optional cursor for
pagination.
Apps have two modes, test and live, each with its own signing secret
(test_secret, prefix ussk_test_; live_secret, prefix ussk_live_). New apps start in
test mode. The typical lifecycle is: create the app, simulate the flow against your callback
URL (sandbox, addressed by appId), rent an extension from your USSD balance, then switch the
app to live. USSD money is a dedicated balance, separate from SMS credit and the main wallet.
using Hellio.Messaging;
var hellio = new HellioClient(token: "your-token-here");
// Pricing and availability
UssdPricing pricing = await hellio.Ussd.PricingAsync();
UssdAvailability check = await hellio.Ussd.AvailabilityAsync("100");
if (check.Valid && check.Available)
{
// check.MonthlyPrice holds the rental cost
}
// 1. Create an application. The response carries both signing secrets and starts in test mode.
UssdApp app = await hellio.Ussd.Apps.CreateAsync("Airtime top-up", "https://your-app.com/ussd");
string appId = app.Id!; // a UUID string
string testSecret = app.TestSecret!; // ussk_test_...: verify sandbox callback signatures
string liveSecret = app.LiveSecret!; // ussk_live_...: verify live callback signatures
// app.Mode == "test", app.IsLive == false
await hellio.Ussd.Apps.UpdateAsync(appId, "Airtime top-up", "https://your-app.com/ussd", active: true);
IReadOnlyList<UssdApp> apps = await hellio.Ussd.Apps.ListAsync();
// 2. Simulate the flow. Always runs in the sandbox (test mode) and is addressed by appId.
// Start with newSession: true, then pass the reference back on later steps.
// serviceCode is optional; omit it to use the shared short code.
UssdSimulateResult step1 = await hellio.Ussd.SimulateAsync(
appId: appId, msisdn: "233241234567", newSession: true);
UssdSimulateResult step2 = await hellio.Ussd.SimulateAsync(
appId: appId, msisdn: "233241234567", input: "1", sessionId: "sess-1");
// step.Message is shown to the subscriber; step.Continue is false when the session ends.
// An app you do not own returns ValidationException (422, error "unknown_app").
// 3. Rent an extension (a dialable suffix under the shared short code). Drawn from your USSD balance.
try
{
UssdExtension ext = await hellio.Ussd.Extensions.RentAsync("100", appId: appId);
// ext.DialString is what subscribers dial, e.g. *920*100#
}
catch (ConflictException) // 409: the code is already taken
{
}
catch (InsufficientBalanceException) // 402 "insufficient_ussd_balance": top up your USSD balance
{
}
IReadOnlyList<UssdExtension> extensions = await hellio.Ussd.Extensions.ListAsync();
// 4. Go live once an extension is in place.
try
{
UssdApp live = await hellio.Ussd.Apps.SetModeAsync(appId, "live"); // live.IsLive == true
}
catch (ExtensionRequiredException) // 402 "extension_required": rent an extension first
{
}
// Rotate a signing secret when needed ("test" or "live").
UssdApp rotated = await hellio.Ussd.Apps.RotateSecretAsync(appId, "live");
string newLiveSecret = rotated.LiveSecret!;
// Sessions
IReadOnlyList<UssdSession> ended = await hellio.Ussd.Sessions.ListAsync(status: "ended");
UssdSession session = await hellio.Ussd.Sessions.GetAsync("99999999-0000-0000-0000-000000000009");
// Clean up
await hellio.Ussd.Extensions.ReleaseAsync("33333333-0000-0000-0000-000000000003");
await hellio.Ussd.Apps.DeleteAsync(appId);
Handling USSD callbacks
When a subscriber dials your extension, Hellio POSTs a JSON body to your app's callback_url:
{ "sessionId": "...", "msisdn": "...", "serviceCode": "...", "input": "...", "sequence": 1, "mode": "..." }
The request is signed with an X-Hellio-Signature header holding
HMAC-SHA256(rawBody, appSecret), where appSecret is the secret for the mode the request
came in on: app.TestSecret for sandbox/simulator traffic and app.LiveSecret for live dials
(the mode field in the body tells you which). Verify it, then reply with
{ "message": ..., "action": ... } where action is continue or end.
using System.Security.Cryptography;
using System.Text;
static bool SignatureIsValid(string rawBody, string signatureHeader, string appSecret)
{
using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(appSecret));
var computed = hmac.ComputeHash(Encoding.UTF8.GetBytes(rawBody));
var expected = Convert.ToHexString(computed).ToLowerInvariant();
return CryptographicOperations.FixedTimeEquals(
Encoding.UTF8.GetBytes(expected),
Encoding.UTF8.GetBytes(signatureHeader));
}
// Then return, for example:
// { "message": "Welcome to Airtime top-up\n1. Buy\n2. Balance", "action": "continue" }
Error handling
Non-2xx responses throw typed exceptions (all extend HellioException). Each carries the
HTTP StatusCode and the parsed Response body; ValidationException exposes field errors
via the Errors property.
| Exception | Status |
|---|---|
InvalidApiTokenException |
401 |
InsufficientBalanceException |
402 |
ExtensionRequiredException |
402 (USSD: SetModeAsync to live before renting an extension) |
ConflictException |
409 |
ValidationException (.Errors) |
422 |
RateLimitException |
429 |
HellioException |
other |
using Hellio.Messaging;
try
{
await hellio.SendSmsAsync("233241234567", "Hi");
}
catch (InsufficientBalanceException)
{
// top up
}
catch (ValidationException ex)
{
// ex.Errors holds the field-level messages
}
Rate limit: 120 requests/minute per token. A RateLimitException (429) is thrown when
you exceed it.
Testing
The client accepts an injected HttpClient, so you can mock the transport in your own tests:
var handler = new YourMockHandler(); // an HttpMessageHandler
var hellio = new HellioClient(token: "test", httpClient: new HttpClient(handler));
See tests/Hellio.Messaging.Tests for a working HttpMessageHandler mock and full coverage.
License
MIT
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net5.0 was computed. net5.0-windows was computed. net6.0 was computed. 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 (>= 8.0.5)
-
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.