DocumentationAPI v1 · 1.3.0

Packages

.NET SDK

Install Labyrinthe.Connect and Labyrinthe.Connect.AspNetCore: async client, dependency injection, webhooks and errors.

Two NuGet packages: Labyrinthe.Connect (.NET Standard 2.0 and .NET 8) for the client, and Labyrinthe.Connect.AspNetCore (.NET 8) for dependency injection and webhooks. Every method is async and accepts a CancellationToken.

Version 1.2.0. Source code, examples and changelog: dotnet folder on GitHub.

Installation#

Shell
dotnet add package Labyrinthe.Connect
dotnet add package Labyrinthe.Connect.AspNetCore   # ASP.NET Core apps

Configuration#

using Labyrinthe.Connect;
using Labyrinthe.Connect.AspNetCore;

// Console app: LABYRINTHE_API_KEY and LABYRINTHE_BASE_URL from the environment
using var client = new LabyrintheClient();

// ASP.NET Core: typed HttpClient registered in DI, configuration section "Labyrinthe"
builder.Services.AddLabyrinthe(builder.Configuration);
// or: builder.Services.AddLabyrinthe(options => options.ApiKey = "lsk_test_xxxxxxxxxxxxxxxx");

// Then inject LabyrintheClient into your services
public sealed class OrderNotifier(LabyrintheClient labyrinthe) { /* … */ }
OptionDefaultRole
ApiKeyLABYRINTHE_API_KEYRequired (Labyrinthe__ApiKey variable in configuration).
BaseUrlLABYRINTHE_BASE_URL, puis productionA bare host gets /api/public/v1.
Timeout30 sPer attempt.
MaxRetries / MaxRetryWait2 / 10 sRetries and longest accepted Retry-After wait.
DefaultCountryCode, DefaultSender243, —Local numbers, default Sender ID.
WebhookSecret, WebhookToleranceLABYRINTHE_WEBHOOK_SECRET, 5 minWebhook endpoint.

Mind the names: the method is AddLabyrinthe and the balance is read from Balance.Available. client.WithRequestId("…") returns a client that sends this X-Request-Id.

Send an SMS#

.NET
using Labyrinthe.Connect;

using var client = new LabyrintheClient(new LabyrintheClientOptions
{
    ApiKey = "lsk_test_xxxxxxxxxxxxxxxx", // or new LabyrintheClient() (LABYRINTHE_API_KEY)
});

var message = await client.Sms.SendAsync(new SendSmsRequest
{
    To = "243812345678",
    Body = "Votre commande 1042 est prête.",
    Reference = "CMD-1042",
    IdempotencyKey = "order-1042-ready", // replaying within 24 h sends nothing again
});

Console.WriteLine($"{message.Id} {message.Status}"); // Queued

Several recipients (up to 1000, all or nothing):

.NET
using Labyrinthe.Connect;

using var client = new LabyrintheClient(new LabyrintheClientOptions
{
    ApiKey = "lsk_test_xxxxxxxxxxxxxxxx", // or new LabyrintheClient() (LABYRINTHE_API_KEY)
});

var batch = await client.Sms.SendManyAsync(new SendManySmsRequest
{
    Recipients = new[] { "243812345678", "0899999999" },
    Body = "Rappel : reunion demain 9h.",
    Sender = "MABOUTIQUE", // an approved Sender ID of the account
    Reference = "RAPPEL-2026-11-06",
    IdempotencyKey = "reminder-2026-11-06",
});

foreach (var message in batch.Messages)
{
    Console.WriteLine($"{message.Recipient} {message.Status}");
}

Estimate the cost, read the balance, list messages:

.NET
using Labyrinthe.Connect;

using var client = new LabyrintheClient(new LabyrintheClientOptions
{
    ApiKey = "lsk_test_xxxxxxxxxxxxxxxx", // or new LabyrintheClient() (LABYRINTHE_API_KEY)
});

var estimate = await client.Sms.EstimateAsync(new EstimateSmsRequest
{
    Body = "Votre code est 482913",
    Recipients = new[] { "243812345678", "0812345678", "0899999999" },
});

Console.WriteLine($"{estimate.Encoding} {estimate.RecipientsCount} {estimate.TotalCredits}");
if (!estimate.IsAffordable) { /* top up before sending */ }
.NET
using Labyrinthe.Connect;

using var client = new LabyrintheClient(new LabyrintheClientOptions
{
    ApiKey = "lsk_test_xxxxxxxxxxxxxxxx", // or new LabyrintheClient() (LABYRINTHE_API_KEY)
});

var balance = await client.Balance.GetAsync();

Console.WriteLine($"{balance.Available} credits available, {balance.Held} held");
.NET
using Labyrinthe.Connect;

using var client = new LabyrintheClient(new LabyrintheClientOptions
{
    ApiKey = "lsk_test_xxxxxxxxxxxxxxxx", // or new LabyrintheClient() (LABYRINTHE_API_KEY)
});

var options = new ListSmsOptions
{
    Status = MessageStatus.Delivered,
    From = DateFilter.Day(2026, 11, 1),
    PerPage = 50,
};

await foreach (var message in client.Sms.ListAutoPagingAsync(options))
{
    Console.WriteLine($"{message.Id} {message.Recipient}");
}

OTP verification#

1. Send the code
using Labyrinthe.Connect;

using var client = new LabyrintheClient(new LabyrintheClientOptions
{
    ApiKey = "lsk_test_xxxxxxxxxxxxxxxx", // or new LabyrintheClient() (LABYRINTHE_API_KEY)
});

try
{
    var otp = await client.Otp.SendAsync(new SendOtpRequest
    {
        Phone = "243812345678",
        Locale = OtpLocale.Fr,
    });
    HttpContext.Session.SetString("otp_id", otp.OtpId); // keep it server-side
    Console.WriteLine(otp.TestCode); // "482913" with a lsk_test_ key, null in live
}
catch (OtpResendTooSoonException error)
{
    Console.WriteLine($"Wait {error.RetryAfter?.TotalSeconds} s");
}
2. Verify the code
using Labyrinthe.Connect;

using var client = new LabyrintheClient(new LabyrintheClientOptions
{
    ApiKey = "lsk_test_xxxxxxxxxxxxxxxx", // or new LabyrintheClient() (LABYRINTHE_API_KEY)
});

var result = await client.Otp.VerifyAsync(otpId, code);

if (result.Verified)
{
    // phone verified: log the user in
}
else if (result.Reason == OtpFailureReason.InvalidCode)
{
    Console.WriteLine($"Wrong code, {result.AttemptsRemaining} attempt(s) left");
}
else
{
    Console.WriteLine($"Ask for a new code: {result.Reason}");
}

Receive webhooks#

.NET
using Labyrinthe.Connect;
using Labyrinthe.Connect.AspNetCore;

var builder = WebApplication.CreateBuilder(args);
// appsettings.json: { "Labyrinthe": { "ApiKey": "lsk_test_…", "WebhookSecret": "…" } }
builder.Services.AddLabyrinthe(builder.Configuration);

var app = builder.Build();

app.MapLabyrintheWebhooks("/webhooks/labyrinthe", (webhookEvent, cancellationToken) =>
{
    switch (webhookEvent)
    {
        case MessageStatusChanged changed:
            Console.WriteLine($"{changed.Data.MessageId} {changed.Data.Status.ToApiString()}");
            break;
        case BalanceLow low:
            Console.WriteLine($"Low balance: {low.Data.Balance} / {low.Data.Threshold}");
            break;
    }
    return Task.CompletedTask;
});

app.Run();

Errors and maintenance#

Maintenance (503 FEATURE_DISABLED and OTP_SERVICE_DISABLED) has its own exception: nothing was sent or debited. The package retries on its own if Retry-After is short (10 s by default); otherwise, queue the send. See Errors.

.NET
using Labyrinthe.Connect;

using var client = new LabyrintheClient(new LabyrintheClientOptions
{
    ApiKey = "lsk_test_xxxxxxxxxxxxxxxx", // or new LabyrintheClient() (LABYRINTHE_API_KEY)
});

try
{
    await client.Sms.SendAsync("243812345678", "Bonjour");
}
catch (FeatureDisabledException error)
{
    // 503 maintenance: nothing was sent, no credit used
    Console.WriteLine($"{error.Feature} {error.BlockedBy} {error.AdminMessage} {error.Until}");
    if (error.RetryAfter is { } wait) ScheduleRetry(wait);
    else QueueForLater();
}
catch (InsufficientCreditsException error)
{
    NotifyTopUp(error.Available, error.Required);
}
catch (RecipientNotSendableException error)
{
    foreach (var rejected in error.Rejected) Console.WriteLine($"{rejected.Recipient} {rejected.Reason}");
}
catch (RateLimitException error)
{
    ScheduleRetry(error.RetryAfter ?? TimeSpan.FromMinutes(1));
}
catch (ApiException error)
{
    Console.Error.WriteLine($"{error.Code} {error.StatusCode} {error.RequestId}");
}

Test mode#

client.IsTestMode tells a lsk_test_… key; otp.TestCode then holds the code (hidden from ToString()). Webhook.Sign(rawBody, secret) builds a valid signature for your tests.