Skip to content

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, provider orange_money_sl
  • Amounts as decimal strings ("150.00"), never floats
  • 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
go get github.com/soamtar/soamtar-go-sdk

Configuration ​

go
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))
OptionDefaultDescription
WithBaseURL(url)SOAMTAR_API_URL, then SOAMTAR_BASE_URL (deprecated name), else https://app.so-amtar.com/v1API root
WithHTTPClient(c)&http.Client{Timeout: 15s}Your own *http.Client
WithTimeout(d)15 sRequest timeout
WithSignRequests(true)offSigns 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 ​

go
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 checkout

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

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

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

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

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

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

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

DefaultWebhookEvents is payment.success, payment.failed, payout.success, payout.failed. Deprecated, still working: Webhooks.Test (use SendTest), Status.Retrieve (use Payments.Status).

Errors ​

go
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).

SOAMTAR Payment API Documentation.