Skip to content

.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, provider orange_money_sl
  • Amounts as decimal (150.00m) or "150.00", never double/float
  • An Idempotency-Key is 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 ​

bash
dotnet add package Soamtar --version 1.1.0

Until the package is published, reference the project from the repository (dotnet add reference path/to/Soamtar.csproj).

Configuration ​

csharp
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 variableUsed for
SOAMTAR_SECRET_KEYSecret key, when none is passed.
SOAMTAR_API_URLBase 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:

csharp
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 ​

csharp
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:

csharp
await soamtar.Payments.CreateAsync(parameters, idempotencyKey: "order-1042-attempt-1");

Other calls:

csharp
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, reserved

Send 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.

csharp
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.

csharp
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) ​

csharp
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 ​

csharp
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 event

Without 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.

SOAMTAR Payment API Documentation.