Skip to content

HellioSolutions/hellio-dotnet

Repository files navigation

Hellio Messaging - Official .NET SDK

tests NuGet Downloads License

.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

About

Official Hellio Messaging .NET SDK (Hellio.Messaging) for SMS, OTP & 2FA, voice, number lookup (HLR) and email verification.

Resources

License

Stars

0 stars

Watchers

0 watching

Forks

Packages

 
 
 

Contributors

Languages