SDK Go
Le module officiel SOAMTAR (github.com/soamtar/soamtar-go-sdk, version 1.1.0) : encaissez et envoyez des paiements avec Orange Money Sierra Leone, en leones (SLE).
- Go 1.18+, bibliothèque standard uniquement
- Valeurs par défaut pour la Sierra Leone : devise
SLE, fournisseurorange_money_sl - Montants en chaînes décimales (
"150.00"), jamais en flottants - En-tête
Idempotency-Keyajouté automatiquement à chaque POST - Vérification de la signature des webhooks (
X-Soamtar-Signature)
Côté serveur uniquement
Le client utilise une clé secrète (sk_test_… ou sk_live_…). Ne l'embarquez jamais dans un navigateur ou une application mobile.
Installation
go get github.com/soamtar/soamtar-go-sdkConfiguration
import soamtar "github.com/soamtar/soamtar-go-sdk"
client, err := soamtar.NewClient("") // lit SOAMTAR_SECRET_KEY
if err != nil {
log.Fatal(err)
}
client, err = soamtar.NewClient("sk_test_...", soamtar.WithTimeout(30*time.Second))| Option | Défaut | Rôle |
|---|---|---|
WithBaseURL(url) | SOAMTAR_API_URL, puis SOAMTAR_BASE_URL (ancien nom, déprécié), sinon https://app.so-amtar.com/v1 | Racine de l'API |
WithHTTPClient(c) | &http.Client{Timeout: 15s} | Votre propre *http.Client |
WithTimeout(d) | 15 s | Délai d'attente des requêtes |
WithSignRequests(true) | désactivé | Signe chaque POST (voir Signature des requêtes) |
Les clés sk_test_ passent par le même hôte que les clés live : les paiements sont simulés et aucun argent ne circule. client.IsTestMode() indique quelle clé est utilisée.
Signature des requêtes
soamtar.NewClient(key, soamtar.WithSignRequests(true)) ajoute Soamtar-Timestamp: <unix> et Soamtar-Signature: v1=hex(HMAC_SHA256(secretKey, "<timestamp>.<METHODE>.<chemin>")) à chaque POST (chemin sans la barre oblique initiale, par exemple v1/payments ; horodatage à ±300 s). C'est un durcissement facultatif : la clé Bearer sur HTTPS vous authentifie déjà. Voir Authentification.
Encaisser un paiement
payment, err := client.Payments.Create(ctx, soamtar.PaymentParams{
MerchantRef: "ORDER-1042",
Amount: "150.00", // Le 150.00
Payer: &soamtar.Payer{Phone: "+23276000001"}, // +232 suivi de 8 chiffres
Description: "Commande n° 1042",
SuccessURL: "https://shop.example.sl/merci",
FailedURL: "https://shop.example.sl/panier",
CallbackURL: "https://shop.example.sl/webhooks/soamtar", // facultatif
}) // Currency "SLE" et Provider "orange_money_sl" par défaut
if err != nil {
return err
}
fmt.Println(payment.Data()["status"]) // pending
http.Redirect(w, r, payment.PaymentURL(), http.StatusSeeOther) // page de paiement hébergéePaymentParams accepte aussi Currency (SLE, USD, EUR), Provider, PaymentMethod, CallbackURL, Metadata et IdempotencyKey (envoyé dans l'en-tête Idempotency-Key ; UUID v4 aléatoire s'il est vide).
CallbackURL (facultatif, HTTPS, paiements et payouts) est une URL de webhook propre à la transaction : SOAMTAR y envoie tous ses événements avec le même payload et la même X-Soamtar-Signature que pour vos endpoints enregistrés (signée avec le secret de votre plus ancien endpoint actif : enregistrez-en au moins un).
Autres appels :
client.Payments.Retrieve(ctx, id, false) // par identifiant ou référence SOAMTAR
client.Payments.Retrieve(ctx, id, true) // attend un statut final (long polling)
client.Payments.Verify(ctx, id) // revérifie le statut auprès d'Orange Money
client.Payments.List(ctx, url.Values{"status": {"success"}, "per_page": {"50"}})
client.Payments.Status(ctx, "TXN-1A2B3C4D-...") // par identifiant ou référence SOAMTAR
client.Balance.Retrieve(ctx)Chaque appel renvoie une soamtar.Response (map[string]interface{}) dotée des méthodes Data() et PaymentURL().
Envoyer un paiement (payout)
client.Payouts.Create(ctx, soamtar.PayoutParams{
MerchantRef: "PAYOUT-77",
Amount: "50.00",
Recipient: soamtar.Recipient{Phone: "+23276000001", Name: "Aminata Kamara"},
Description: "Paiement fournisseur",
IdempotencyKey: "payout-PAYOUT-77",
})
client.Payouts.Retrieve(ctx, id)
client.Payouts.List(ctx, url.Values{"status": {"success"}})Remboursements
Remboursez un paiement réussi, en totalité ou en partie : SOAMTAR recrédite le montant sur le numéro Orange Money du payeur et débite votre solde. La clé doit disposer du scope can_refund.
refund, err := client.Refunds.Create(ctx, soamtar.RefundParams{
TransactionID: paymentID, // identifiant (ou référence) SOAMTAR du paiement
Amount: "50.00", // vide : remboursement total du restant
Reason: "customer_request",
Description: "Article en rupture de 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) // identifiant ou référence
client.Refunds.List(ctx, url.Values{"transaction_id": {paymentID}, "status": {"success"}})Le remboursement est soumis dans la même requête : success avec une clé de test, processing avec Orange Money jusqu'à la confirmation d'Orange, failed (avec failure_reason) si le crédit est refusé. Le résultat arrive par refund.success / refund.failed, plus payment.refunded / payment.partially_refunded en cas de succès. Voir Remboursements pour l'objet et les codes d'erreur.
Webhooks
SOAMTAR envoie les événements aux endpoints que vous enregistrez (tableau de bord ou Webhooks.Create), signés avec le secret de l'endpoint : X-Soamtar-Signature: t=<unix>,v1=<hmac> (HMAC-SHA256 de "<t>.<corps brut>"). Pendant une rotation du secret, une signature v2= est ajoutée ; l'une ou l'autre est acceptée. Les signatures de plus de 300 secondes sont rejetées.
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 { // enveloppe 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 et le reste : simple 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)
})Un même événement peut être livré plusieurs fois : dédoublonnez sur event + data.id.
Événements auxquels s'abonner : 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 est envoyé par client.Webhooks.SendTest et par le bouton de test du tableau de bord, quels que soient vos abonnements. Voir Webhooks.
Gérer les endpoints
client.Webhooks.Create(ctx, soamtar.WebhookEndpointParams{
URL: "https://shop.example.sl/webhooks/soamtar", // SubscribedEvents vide = 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) // secret de signature
client.Webhooks.SendTest(ctx) // envoie un événement webhook.testDefaultWebhookEvents vaut payment.success, payment.failed, payout.success, payout.failed. Dépréciés mais toujours fonctionnels : Webhooks.Test (utilisez SendTest), Status.Retrieve (utilisez Payments.Status).
Erreurs
var apiErr *soamtar.APIError
if errors.As(err, &apiErr) {
log.Println(apiErr.StatusCode, apiErr.Code, apiErr.Message)
log.Println(apiErr.Errors) // messages par champ en cas de 422
}Les autres erreurs (clé invalide, montant invalide, réseau) sont des error simples préfixées par soamtar:.
Sécurité
- Comparaison des signatures en temps constant (
hmac.Equal). - La clé secrète est masquée par
String()et%#v. - Les webhooks de plus de 1 Mo sont rejetés (
soamtar.MaxPayloadSize).