Go SDK
The official SOAMTAR module (github.com/soamtar/soamtar-go-sdk, version 1.1.0): collect payments and send payouts with Orange Money Sierra Leone, in leones (SLE).
- Go 1.18+, standard library only
- Sierra Leone defaults: currency
SLE, providerorange_money_sl - Amounts as decimal strings (
"150.00"), never floats - 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
go get github.com/soamtar/soamtar-go-sdkConfiguration
import soamtar "github.com/soamtar/soamtar-go-sdk"
client, err := soamtar.NewClient("") // reads SOAMTAR_SECRET_KEY
if err != nil {
log.Fatal(err)
}
client, err = soamtar.NewClient("sk_test_...", soamtar.WithTimeout(30*time.Second))| Option | Default | Description |
|---|---|---|
WithBaseURL(url) | SOAMTAR_API_URL, then SOAMTAR_BASE_URL (deprecated name), else https://app.so-amtar.com/v1 | API root |
WithHTTPClient(c) | &http.Client{Timeout: 15s} | Your own *http.Client |
WithTimeout(d) | 15 s | Request timeout |
WithSignRequests(true) | off | Signs every POST (see Request signing) |
sk_test_ keys run on the same host as live keys: payments are simulated and no money moves. client.IsTestMode() tells you which key is in use.
Request signing
soamtar.NewClient(key, soamtar.WithSignRequests(true)) adds Soamtar-Timestamp: <unix> and Soamtar-Signature: v1=hex(HMAC_SHA256(secretKey, "<timestamp>.<METHOD>.<path>")) to every POST (path without the leading slash, e.g. v1/payments; timestamp within ±300 s). It is optional hardening: the Bearer key over HTTPS already authenticates you. See Authentication.
Collect a payment
payment, err := client.Payments.Create(ctx, soamtar.PaymentParams{
MerchantRef: "ORDER-1042",
Amount: "150.00", // Le 150.00
Payer: &soamtar.Payer{Phone: "+23276123456"}, // +232 followed by 8 digits
Description: "Order #1042",
SuccessURL: "https://shop.example.sl/thanks",
FailedURL: "https://shop.example.sl/cart",
CallbackURL: "https://shop.example.sl/webhooks/soamtar", // optional
}) // Currency "SLE" and Provider "orange_money_sl" by default
if err != nil {
return err
}
fmt.Println(payment.Data()["status"]) // pending
http.Redirect(w, r, payment.PaymentURL(), http.StatusSeeOther) // hosted checkoutPaymentParams also has Currency (SLE, USD, EUR), Provider, PaymentMethod, CallbackURL, Metadata and IdempotencyKey (sent as the Idempotency-Key header; a random UUID v4 when empty).
CallbackURL (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).
Other calls:
client.Payments.Retrieve(ctx, id, false) // by SOAMTAR id or reference
client.Payments.Retrieve(ctx, id, true) // long poll until a final status
client.Payments.Verify(ctx, id) // re-check the status with Orange Money
client.Payments.List(ctx, url.Values{"status": {"success"}, "per_page": {"50"}})
client.Payments.Status(ctx, "TXN-1A2B3C4D-...") // by SOAMTAR id or reference
client.Balance.Retrieve(ctx)Every call returns a soamtar.Response (map[string]interface{}) with Data() and PaymentURL() helpers.
Send a payout
client.Payouts.Create(ctx, soamtar.PayoutParams{
MerchantRef: "PAYOUT-77",
Amount: "50.00",
Recipient: soamtar.Recipient{Phone: "+23276123456", Name: "Aminata Kamara"},
Description: "Supplier payment",
})
client.Payouts.Retrieve(ctx, id)
client.Payouts.List(ctx, url.Values{"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.
refund, err := client.Refunds.Create(ctx, soamtar.RefundParams{
TransactionID: paymentID, // SOAMTAR id (or reference) of the payment
Amount: "50.00", // empty for a full refund of what remains
Reason: "customer_request",
Description: "Item out of stock",
IdempotencyKey: "refund-order-1042-1",
})
if err != nil {
return err
}
fmt.Println(refund.Data()["reference"], refund.Data()["status"]) // REF-…, processing
client.Refunds.Retrieve(ctx, refundID) // id or reference
client.Refunds.List(ctx, url.Values{"transaction_id": {paymentID}, "status": {"success"}})The refund is submitted in the same request: success with a test key, processing with Orange Money until Orange confirms, failed (with failure_reason) if the credit is refused. 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.Create), 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.
net/http
http.HandleFunc("/webhooks/soamtar", func(w http.ResponseWriter, r *http.Request) {
payload, err := io.ReadAll(http.MaxBytesReader(w, r.Body, soamtar.MaxPayloadSize))
if err != nil {
http.Error(w, "payload too large", http.StatusRequestEntityTooLarge)
return
}
event, err := soamtar.ConstructEvent(payload, r.Header.Get(soamtar.SignatureHeader), os.Getenv("SOAMTAR_WEBHOOK_SECRET"))
if err != nil { // wraps soamtar.ErrInvalidSignature
http.Error(w, "invalid signature", http.StatusBadRequest)
return
}
switch event.Type() {
case "payment.success":
markOrderPaid(event.Data()["merchant_reference"])
case "payment.failed", "payment.expired", "payment.cancelled":
markOrderFailed(event.Data()["merchant_reference"])
case "refund.success":
markOrderRefunded(event.Data()["merchant_reference"], event.Data()["amount"])
} // webhook.test and others: just 200
w.WriteHeader(http.StatusOK)
})Gin
r.POST("/webhooks/soamtar", func(c *gin.Context) {
payload, _ := io.ReadAll(http.MaxBytesReader(c.Writer, c.Request.Body, soamtar.MaxPayloadSize))
event, err := soamtar.ConstructEvent(payload, c.GetHeader(soamtar.SignatureHeader), os.Getenv("SOAMTAR_WEBHOOK_SECRET"))
if err != nil {
c.Status(http.StatusBadRequest)
return
}
_ = event // event.Type(), event.Data()
c.Status(http.StatusOK)
})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 client.Webhooks.SendTest and the dashboard's test button whatever your subscriptions. See Webhooks.
Manage endpoints
client.Webhooks.Create(ctx, soamtar.WebhookEndpointParams{
URL: "https://shop.example.sl/webhooks/soamtar", // empty SubscribedEvents = DefaultWebhookEvents
})
client.Webhooks.Create(ctx, soamtar.WebhookEndpointParams{
URL: "https://shop.example.sl/webhooks/soamtar",
SubscribedEvents: []string{"payment.success", "refund.success", "refund.failed"},
})
client.Webhooks.List(ctx)
client.Webhooks.Lookup(ctx, "https://shop.example.sl/webhooks/soamtar")
client.Webhooks.Secret(ctx, endpointID) // signing secret
client.Webhooks.SendTest(ctx) // sends a webhook.test eventDefaultWebhookEvents is payment.success, payment.failed, payout.success, payout.failed. Deprecated, still working: Webhooks.Test (use SendTest), Status.Retrieve (use Payments.Status).
Errors
var apiErr *soamtar.APIError
if errors.As(err, &apiErr) {
log.Println(apiErr.StatusCode, apiErr.Code, apiErr.Message)
log.Println(apiErr.Errors) // field messages on 422
}Other errors (invalid key, invalid amount, network) are plain error values prefixed with soamtar:.
Security
- Constant-time signature comparison (
hmac.Equal). - The secret key is masked by
String()and%#v. - Webhook payloads over 1 MB are rejected (
soamtar.MaxPayloadSize).