.NET (C#) SDK
The official SOAMTAR library for .NET (Soamtar package 1.1.0, namespace Soamtar): collect payments and send payouts with Orange Money Sierra Leone, in leones (SLE).
- .NET 8, no dependencies beyond the base library (
System.Text.Json) - Sierra Leone defaults: currency
SLE, providerorange_money_sl - Amounts as
decimal(150.00m) or"150.00", neverdouble/float - An
Idempotency-Keyis sent automatically on every POST - Webhook signature verification (
X-Soamtar-Signature)
Server-side only
The client takes a secret key (sk_test_… or sk_live_…). Never ship it to a browser or a mobile app.
Installation
dotnet add package Soamtar --version 1.1.0Until the package is published, reference the project from the repository (dotnet add reference path/to/Soamtar.csproj).
Configuration
using Soamtar;
var soamtar = new SoamtarClient(); // reads SOAMTAR_SECRET_KEY and SOAMTAR_API_URL
var explicitClient = new SoamtarClient(Environment.GetEnvironmentVariable("SOAMTAR_SECRET_KEY")!);
// Your own HttpClient (e.g. from IHttpClientFactory), another base URL, optional request signing
var custom = new SoamtarClient(secretKey, SoamtarClient.DefaultBaseUrl, httpClient, signRequests: true);| Environment variable | Used for |
|---|---|
SOAMTAR_SECRET_KEY | Secret key, when none is passed. |
SOAMTAR_API_URL | Base URL (default https://app.so-amtar.com/v1). SOAMTAR_BASE_URL is still read as a fallback (deprecated name). |
All constructor arguments are optional; explicit values always win over the environment.
With ASP.NET Core, register one instance:
builder.Services.AddSingleton(new SoamtarClient(builder.Configuration["Soamtar:SecretKey"]!));sk_test_ keys run on the same host as live keys (https://app.so-amtar.com/v1): payments are simulated and no money moves. soamtar.IsTestMode tells you which key is in use.
Request signing
new SoamtarClient(signRequests: true) adds Soamtar-Timestamp: <unix> and Soamtar-Signature: v1=hex(HMAC_SHA256(secretKey, "<timestamp>.<METHOD>.<path>")) to every POST, for example over 1700000000.POST.v1/payments (path without the leading slash; timestamp within ±300 s). SoamtarClient.SignRequest(secretKey, timestamp, method, path) exposes the computation. It is optional hardening: the Bearer key over HTTPS already authenticates you. See Authentication.
Collect a payment
try
{
var response = await soamtar.Payments.CreateAsync(new Dictionary<string, object?>
{
["merchant_ref"] = "ORDER-1042",
["amount"] = 150.00m, // Le 150.00
["phone"] = "+23276123456", // +232 followed by 8 digits
["description"] = "Order #1042",
["success_url"] = "https://shop.example.sl/thanks",
["failed_url"] = "https://shop.example.sl/cart",
["callback_url"] = "https://shop.example.sl/webhooks/soamtar", // optional
}); // currency SLE, provider orange_money_sl by default
var payment = response.GetProperty("data");
var status = payment.GetProperty("status").GetString(); // "pending"
var paymentUrl = payment.GetProperty("payment_url").GetString(); // hosted checkout page
}
catch (SoamtarException e)
{
Console.Error.WriteLine($"{e.HttpStatus} {e.Code} {e.Message}");
}merchant_ref and amount are checked before anything is sent (ArgumentException).
callback_url (optional, HTTPS, payments and payouts) is a per-payment webhook URL: SOAMTAR sends it every event of that transaction with the same payload and X-Soamtar-Signature as your registered endpoints (signed with the secret of your oldest active endpoint, so register at least one). Verify it the same way.
Calls return a System.Text.Json.JsonElement (the decoded {"data": …} body). Pass your own Idempotency-Key to make retries safe:
await soamtar.Payments.CreateAsync(parameters, idempotencyKey: "order-1042-attempt-1");Other calls:
await soamtar.Payments.RetrieveAsync(id); // by SOAMTAR id or reference
await soamtar.Payments.RetrieveAsync(id, wait: true); // long poll until a final status
await soamtar.Payments.VerifyAsync(id); // re-check the status with Orange Money
await soamtar.Payments.ListAsync(new Dictionary<string, object?> { ["status"] = "success", ["per_page"] = 50 });
await soamtar.Payments.StatusAsync("TXN-1A2B3C4D-..."); // by SOAMTAR id or reference
await soamtar.Balance.RetrieveAsync(); // available, pending, reservedSend a payout
Payouts require a key with the payout permission. merchant_ref, amount and recipient.phone (+232 followed by 8 digits) are validated before sending.
await soamtar.Payouts.CreateAsync(new Dictionary<string, object?>
{
["merchant_ref"] = "PAYOUT-77",
["amount"] = 50.00m,
["recipient"] = new Dictionary<string, object?> { ["phone"] = "+23276123456", ["name"] = "Aminata Kamara" },
});
await soamtar.Payouts.RetrieveAsync(id);
await soamtar.Payouts.ListAsync(new Dictionary<string, object?> { ["status"] = "success" });Refunds
Refund a successful payment, fully or partially: SOAMTAR credits the amount back to the payer's Orange Money number and debits your balance. The key needs the can_refund scope.
var response = await soamtar.Refunds.CreateAsync(new Dictionary<string, object?>
{
["transaction_id"] = paymentId, // SOAMTAR id (or reference) of the payment
["amount"] = 50.00m, // optional: omit for a full refund of what remains
["reason"] = "customer_request", // optional
["description"] = "Item out of stock", // optional
}, idempotencyKey: "refund-order-1042-1");
var refund = response.GetProperty("data");
refund.GetProperty("reference").GetString(); // "REF-…"
refund.GetProperty("status").GetString(); // "success" (test key), "processing" (Orange Money) or "failed"
await soamtar.Refunds.RetrieveAsync("REF-…"); // id or reference
await soamtar.Refunds.ListAsync(new Dictionary<string, object?> { ["transaction_id"] = paymentId, ["status"] = "success" });The refund is submitted in the same request; with Orange Money it stays processing until Orange confirms the credit. The outcome arrives with refund.success / refund.failed, plus payment.refunded / payment.partially_refunded on success. See Refunds for the object and error codes.
Webhooks
SOAMTAR posts events to the endpoints you register (dashboard or Webhooks.CreateAsync), signed with that endpoint's secret: X-Soamtar-Signature: t=<unix>,v1=<hmac> (HMAC-SHA256 of "<t>.<raw body>"). During a secret rotation a v2= signature is added; either one is accepted. Signatures older than 300 seconds are rejected.
ASP.NET Core (minimal API)
app.MapPost("/webhooks/soamtar", async (HttpRequest request) =>
{
using var reader = new StreamReader(request.Body);
var payload = await reader.ReadToEndAsync(); // RAW body
try
{
var evt = Webhook.ConstructEvent(payload, request.Headers[Webhook.SignatureHeader],
Environment.GetEnvironmentVariable("SOAMTAR_WEBHOOK_SECRET")!);
var data = evt.GetProperty("data");
switch (evt.GetProperty("event").GetString())
{
case "payment.success":
MarkPaid(data.GetProperty("merchant_reference").GetString());
break;
case "payment.failed":
case "payment.expired":
case "payment.cancelled":
MarkFailed(data.GetProperty("merchant_reference").GetString());
break;
case "refund.success":
MarkRefunded(data.GetProperty("merchant_reference").GetString(), data.GetProperty("amount").GetString());
break;
// webhook.test and others: just 200
}
return Results.Ok();
}
catch (WebhookSignatureException)
{
return Results.BadRequest("Invalid signature");
}
});The same event can be delivered more than once: deduplicate on event + data.id.
Events you can subscribe to: payment.success, payment.failed, payment.cancelled, payment.expired, payment.refunded, payment.partially_refunded, payout.success, payout.failed, payout.cancelled, refund.success, refund.failed, payout_request.submitted, payout_request.approved, payout_request.rejected, payout_request.completed, payout_request.failed. webhook.test is sent by soamtar.Webhooks.SendTestAsync() and the dashboard's test button whatever your subscriptions. See Webhooks.
Manage endpoints
await soamtar.Webhooks.CreateAsync("https://shop.example.sl/webhooks/soamtar"); // default events
await soamtar.Webhooks.CreateAsync("https://shop.example.sl/webhooks/soamtar",
new[] { "payment.success", "payment.failed", "refund.success", "refund.failed" });
await soamtar.Webhooks.ListAsync();
await soamtar.Webhooks.SecretAsync(endpointId); // signing secret
await soamtar.Webhooks.SendTestAsync(); // sends a webhook.test eventWithout an event list (or with null / an empty list), CreateAsync() subscribes to payment.success, payment.failed, payout.success and payout.failed (Webhooks.DefaultEvents), like the other SDKs.
Errors
API errors throw SoamtarException with HttpStatus, Code (for example validation_error) and the raw Body. Network failures use status 0 and code network_error. An invalid key, a double amount or a decimal with more than 2 decimals throws ArgumentException.
Security
- Constant-time signature comparison.
- The secret key is masked in
ToString(). - Webhook payloads over 1 MB are rejected.