SDKs and tools
.NET and ASP.NET Core SDK
Install and use Labyrinthe.LabPay (.NET Standard 2.0 / .NET 8) and Labyrinthe.LabPay.AspNetCore: async client, AddLabPay() and MapLabPayWebhooks().
Labyrinthe.LabPay: netstandard2.0 and net8.0, HttpClient, System.Text.Json (no reflection: trimming / AOT friendly on net8.0), async API with CancellationToken. Labyrinthe.LabPay.AspNetCore (net8.0): services.AddLabPay(configuration) (typed IHttpClientFactory client, Microsoft.Extensions.Logging logs) and app.MapLabPayWebhooks().
Version 0.1.0 (preview). Source code, examples, changelog and full README: dotnet folder on GitHub.
Installation#
dotnet add package Labyrinthe.LabPay
dotnet add package Labyrinthe.LabPay.AspNetCore # intégration ASP.NET CoreThis SDK is not published on NuGet yet: the command above will work once it is released. Until then, install it from the SDK GitHub repository, as shown below.
From the repository: reference the projects of src/ (<ProjectReference>), or pack them locally with dotnet pack (.NET 8 SDK).
git clone https://github.com/Labyrinthe-Team/labyrinthe-pay-packages.git
dotnet add reference ../labyrinthe-pay-packages/dotnet/src/Labyrinthe.LabPay/Labyrinthe.LabPay.csproj
dotnet add reference ../labyrinthe-pay-packages/dotnet/src/Labyrinthe.LabPay.AspNetCore/Labyrinthe.LabPay.AspNetCore.csprojConfiguration#
using Labyrinthe.LabPay;
// Depuis l'environnement : LABPAY_API_KEY (et LABPAY_BASE_URL)
using var labpay = new LabPayClient();
// Ou explicitement
using var client = new LabPayClient(new LabPayClientOptions
{
ApiKey = "test_xxxxxxxxxxxxxxxx",
BaseUrl = "https://api.labyrinthe-rdc.com/api", // défaut ; "/V2" est ajouté
Timeout = TimeSpan.FromSeconds(30), // par tentative
MaxRetries = 2,
MaxRetryWait = TimeSpan.FromSeconds(10),
AutoReference = true,
Logger = LabPayLogger.Create((level, message) => Console.Error.WriteLine(message)),
});| Option | Default | Role |
|---|---|---|
ApiKey | LABPAY_API_KEY | Live token or test_… test key. |
BaseUrl | LABPAY_BASE_URL | API root; /V2 is added. |
Timeout | 30 s | Per attempt. |
MaxRetries / MaxRetryWait | 2 / 10 s | Safe retries and longest Retry-After wait. |
AutoReference | true | Generates sdk_<uuid> when Reference is absent. |
Logger | — | Logger; never contains the key. |
LabPayClient is thread-safe: create one and reuse it (or let AddLabPay manage it). Your own HttpClient goes as second argument (it must not follow redirects). Request amounts as decimal (at most 2 decimals); in responses, strings (payment.AmountValue: exact decimal).
ASP.NET Core#
{
"LabPay": {
"ApiKey": "test_xxxxxxxxxxxxxxxx",
"WebhookSecret": "whsec_xxxxxxxxxxxxxxxx"
}
}First collection (test mode)#
With the test key and the test number 0813456789 (success), no real money moves. The payment stays pending until the customer confirms; only deliver on completed.
using Labyrinthe.LabPay;
using var labpay = new LabPayClient(new LabPayClientOptions { ApiKey = "test_xxxxxxxxxxxxxxxx" }); // ou new LabPayClient() (LABPAY_API_KEY)
var payment = await labpay.Deposits.CreateAsync(new MobileMoneyRequest
{
Amount = 1000m,
Currency = "CDF",
Phone = "0813456789",
Reference = "ORDER-42", // facultatif : sinon le SDK génère sdk_<uuid>
CallbackUrl = "https://boutique.example/webhooks/labpay",
CustomerMessage = "Commande 42",
Metadata = new Dictionary<string, object?> { ["orderId"] = "42" },
});
Console.WriteLine($"{payment.Id} {payment.Status}"); // DEP-… PendingWithout reference, the SDK generates one (sdk_<uuid>): a resend after a network failure returns the same payment.
using Labyrinthe.LabPay;
using var labpay = new LabPayClient(new LabPayClientOptions { ApiKey = "test_xxxxxxxxxxxxxxxx" }); // ou new LabPayClient() (LABPAY_API_KEY)
// Sans Reference : le SDK envoie sdk_<uuid> et peut relancer sans risque de double paiement.
var payment = await labpay.Deposits.CreateAsync(new MobileMoneyRequest { Amount = 1000m, Currency = "CDF", Phone = "0813456789" });
Console.WriteLine(payment.Reference); // sdk_…
// Désactiver : new LabPayClientOptions { ApiKey = "test_xxxxxxxxxxxxxxxx", AutoReference = false }Track and synchronise#
The final status arrives by notification. As a safety net, read the payment, or force a refresh from LabPay with sync (without a tight loop).
using Labyrinthe.LabPay;
using var labpay = new LabPayClient(new LabPayClientOptions { ApiKey = "test_xxxxxxxxxxxxxxxx" }); // ou new LabPayClient() (LABPAY_API_KEY)
var payment = await labpay.Payments.GetAsync("DEP-01JCX8Q3T6Y0V5W1Z2A3B4C5D6");
if (payment.IsCompleted)
{
// livrer la commande payment.Reference
}using Labyrinthe.LabPay;
using var labpay = new LabPayClient(new LabPayClientOptions { ApiKey = "test_xxxxxxxxxxxxxxxx" }); // ou new LabPayClient() (LABPAY_API_KEY)
var payment = await labpay.Payments.SyncAsync("DEP-01JCX8Q3T6Y0V5W1Z2A3B4C5D6"); // relit le statut tout de suiteusing Labyrinthe.LabPay;
using var labpay = new LabPayClient(new LabPayClientOptions { ApiKey = "test_xxxxxxxxxxxxxxxx" }); // ou new LabPayClient() (LABPAY_API_KEY)
var page = await labpay.Payments.ListAsync(new ListPaymentsRequest
{
Status = PaymentStatus.Completed,
Operation = Operation.Deposit,
PerPage = 50,
});
foreach (var payment in page.Data) Console.WriteLine($"{payment.Id} {payment.Amount}");
// Toutes les pages, à la demande :
await foreach (var payment in labpay.Payments.IterateAsync(new ListPaymentsRequest { Status = PaymentStatus.Completed })) { }
// Par votre référence (null si inconnue) :
var found = await labpay.Payments.FindByReferenceAsync("ORDER-42");Send money#
Your balance is debited with amount + fees at initiation, and credited back if the payout fails. A queued payout (ENQUEUED) can be cancelled.
using Labyrinthe.LabPay;
using var labpay = new LabPayClient(new LabPayClientOptions { ApiKey = "test_xxxxxxxxxxxxxxxx" }); // ou new LabPayClient() (LABPAY_API_KEY)
try
{
var payout = await labpay.Payouts.CreateAsync(new MobileMoneyRequest
{
Amount = 5000m,
Currency = "CDF",
Phone = "0813456789",
Reference = "PAYOUT-17",
CallbackUrl = "https://boutique.example/webhooks/labpay",
Metadata = new Dictionary<string, object?> { ["payoutId"] = "17" },
});
Console.WriteLine($"{payout.Id} {payout.TotalAmount}"); // montant + frais débités
}
catch (InsufficientBalanceException)
{
// rien n'a été envoyé
}using Labyrinthe.LabPay;
using var labpay = new LabPayClient(new LabPayClientOptions { ApiKey = "test_xxxxxxxxxxxxxxxx" }); // ou new LabPayClient() (LABPAY_API_KEY)
var payout = await labpay.Payments.GetAsync("PAY-01JCX9A1B2C3D4E5F6G7H8J9K0");
if (payout.IsQueued) // envoi ENQUEUED : annulable
{
payout = await labpay.Payouts.CancelAsync(payout.Id);
}Refund#
All or part of a completed deposit, to the wallet that paid; only one refund in progress at a time per deposit.
using Labyrinthe.LabPay;
using var labpay = new LabPayClient(new LabPayClientOptions { ApiKey = "test_xxxxxxxxxxxxxxxx" }); // ou new LabPayClient() (LABPAY_API_KEY)
var refund = await labpay.Refunds.CreateAsync(new RefundRequest
{
PaymentId = "DEP-01JCX8Q3T6Y0V5W1Z2A3B4C5D6",
Amount = 300m, // facultatif : par défaut, tout le reste remboursable
Reference = "REFUND-42-1",
});Providers, prediction and balances#
using Labyrinthe.LabPay;
using var labpay = new LabPayClient(new LabPayClientOptions { ApiKey = "test_xxxxxxxxxxxxxxxx" }); // ou new LabPayClient() (LABPAY_API_KEY)
var prediction = await labpay.Providers.PredictAsync("0813456789", "COD");
Console.WriteLine(prediction.Provider); // VODACOM_MPESA_CODusing Labyrinthe.LabPay;
using var labpay = new LabPayClient(new LabPayClientOptions { ApiKey = "test_xxxxxxxxxxxxxxxx" }); // ou new LabPayClient() (LABPAY_API_KEY)
var providers = await labpay.Providers.ListAsync(Operation.Payout, "COD");using Labyrinthe.LabPay;
using var labpay = new LabPayClient(new LabPayClientOptions { ApiKey = "test_xxxxxxxxxxxxxxxx" }); // ou new LabPayClient() (LABPAY_API_KEY)
var balances = await labpay.Account.BalancesAsync();
var cdf = await labpay.Account.BalanceAsync("CDF"); // null si aucun solde dans cette devise
Console.WriteLine(cdf?.Amount); // "125000.00" (cdf.Value : decimal exact)Also: open countries (GET /V2/countries), availability (GET /V2/availability), account (GET /V2/account) and rotations of the signing secret and test key (live token). Examples on Providers and prediction and Account and balances.
Error handling#
Every error extends LabPayException; API errors extend ApiException (code: e.Code, HTTP status, per-field errors, Retry-After, mode). A payment refused at initiation is not an error: it is returned with the failed status. Full list of codes: Errors.
using Labyrinthe.LabPay;
using var labpay = new LabPayClient(new LabPayClientOptions { ApiKey = "test_xxxxxxxxxxxxxxxx" }); // ou new LabPayClient() (LABPAY_API_KEY)
try
{
var payment = await labpay.Deposits.CreateAsync(new MobileMoneyRequest
{ Amount = 1000m, Currency = "CDF", Phone = "0813456789", Reference = "ORDER-42" });
if (payment.IsFailed)
{
// refusé dès l'initiation (422 avec le paiement) : renvoyé, pas levé
Console.WriteLine(payment.Failure?.Code);
}
}
catch (ValidationException e) { Console.WriteLine(string.Join(", ", e.Errors.Keys)); } // champs en erreur
catch (ServiceUnavailableException e) { Console.WriteLine($"{e.Code} {e.RetryAfter}"); } // MAINTENANCE…
catch (ConnectionException) { /* PAS un échec : renvoyer la même référence */ }
catch (ApiException e) { Console.WriteLine(e.Code); } // toujours le code, jamais le message| Code or case | Class |
|---|---|
UNAUTHENTICATED (401) | AuthenticationException |
TOKEN_BLOCKED, MERCHANT_INACTIVE, KYC_REQUIRED, LIVE_TOKEN_REQUIRED (403) | PermissionException |
PAYMENT_NOT_FOUND, DEPOSIT_NOT_FOUND, NOTIFICATION_NOT_FOUND (404) | NotFoundException |
NOT_A_PAYOUT, NOT_CANCELLABLE, DEPOSIT_NOT_COMPLETED, REFUND_IN_PROGRESS, DEPOSIT_ALREADY_REFUNDED (409) | ConflictException |
VALIDATION_ERROR (422) | ValidationException |
INVALID_PHONE_NUMBER | InvalidPhoneNumberException |
AMOUNT_TOO_SMALL, AMOUNT_TOO_LARGE, AMOUNT_OUT_OF_BOUNDS | AmountOutOfRangeException |
INSUFFICIENT_BALANCE | InsufficientBalanceException |
INVALID_CURRENCY, UNSUPPORTED_COUNTRY, UNSUPPORTED_PROVIDER, INVALID_AMOUNT | InvalidRequestException |
429 (rate limit) / DAILY_LIMIT_REACHED | RateLimitException / DailyLimitReachedException |
MAINTENANCE, API_DISABLED, DEPOSITS_DISABLED, WITHDRAWALS_DISABLED, PROVIDER_TEMPORARILY_UNAVAILABLE, GATEWAY_UNAVAILABLE, V2_DISABLED, TEST_MODE_DISABLED, other 5xx | ServiceUnavailableException |
| Network error or timeout | ConnectionException |
| Invalid input, detected before any call | InvalidArgumentException |
| Notification with an invalid signature or body | SignatureVerificationException / InvalidWebhookPayloadException |
The API_VERSION_DISABLED and FEATURE_DISABLED codes (version or operation switched off from the console, 503) are newer than SDK 0.1.0: they arrive as an ApiException of the 503 (service unavailable) class, with their code intact. Do not retry in a loop: see Suspended features.
Receive notifications (webhooks)#
app.MapLabPayWebhooks() exposes POST /webhooks/labpay: raw body verified, then your ILabPayWebhookHandler is called. Answers: 400 (invalid signature or body), 413 (above WebhookMaxBodySize), 500 when the handler throws (LabPay delivers it again later). WebhookSecret accepts several comma-separated secrets (default: LABPAY_WEBHOOK_SECRET). Outside ASP.NET Core: Webhook.ConstructEvent(rawBodyBytes, signatureHeader, secret); Webhook.Sign(body, secret) for your tests.
// appsettings.json : { "LabPay": { "ApiKey": "test_xxxxxxxxxxxxxxxx", "WebhookSecret": "whsec_xxxxxxxxxxxxxxxx" } }
builder.Services.AddLabPay(builder.Configuration);
builder.Services.AddLabPayWebhookHandler<PaymentHandler>();
var app = builder.Build();
app.MapLabPayWebhooks(); // POST /webhooks/labpay : corps brut vérifié, puis PaymentHandler
sealed class PaymentHandler : ILabPayWebhookHandler
{
public Task HandleAsync(WebhookEvent webhookEvent, CancellationToken cancellationToken)
{
if (webhookEvent.IsPaymentCompleted) { /* livrer webhookEvent.Payment.Reference une seule fois */ }
return Task.CompletedTask;
}
}
// Ailleurs : Webhook.ConstructEvent(rawBodyBytes, signatureHeader, secret)- The
whsec_…secret is read onGET /V2/accountwith the live token (LABPAY_WEBHOOK_SECRETvariable). - Invalid signature or body: answer
400. Otherwise200within 10 seconds, long work afterwards. - The same notification can arrive more than once: deduplicate on
X-Labyrinthe-Deliveryor the paymentid.
History and resend#
using Labyrinthe.LabPay;
using var labpay = new LabPayClient(new LabPayClientOptions { ApiKey = "test_xxxxxxxxxxxxxxxx" }); // ou new LabPayClient() (LABPAY_API_KEY)
var page = await labpay.Notifications.ListAsync(new ListNotificationsRequest { Status = NotificationStatus.Abandoned });
foreach (var notification in page.Data)
{
await labpay.Notifications.ResendAsync(notification.Id); // nouvelle notification, historique conservé
}Shared behaviour#
- `test_…` key = sandbox: no real money, separate balances and payments. Going live = switching keys.
- Automatic reference: without
reference, the SDK sendssdk_<uuid>, so that a deposit, payout or refund can be retried after a network failure or a 5xx without double payment (idempotency). Can be disabled (autoReference/auto_reference/AutoReference). - A refused payment is a payment: a
422carrying the payment (refused at initiation) is returned withstatus: failedandfailure, not thrown; a202is apendingpayment. - Errors typed by `code` (then by HTTP status), with per-field validation errors; an unknown code stays readable. Base your logic on the code, never on the message (in French).
- Safe retries (2 by default): reads and payments with a reference,
429afterRetry-After, maintenance-type refusals (503) only after aRetry-After; neverDAILY_LIMIT_REACHED. Rotations, notification resends, payout cancellations and payments without reference are never retried after a network error or a 5xx. - Local validation before any request (amount, currency, number, reference, URL…): invalid input raises an argument error with no network call.
- Verified webhooks:
constructEventchecksX-Labyrinthe-Signatureon the raw body, in constant time, with a 300 s tolerance and both secrets during a rotation. - Secure by default: the key never shows up in logs, dumps or exception messages.
See also: all SDKs and tools, V2 endpoint reference, error codes, Postman and OpenAPI.