SDK Python
Client Python officiel de l'API SOAMTAR : encaissez et envoyez des fonds avec Orange Money Sierra Leone, en leones (SLE).
- Paquet :
soamtar(version 1.1.0). Python 3.8+, bibliothèque standard uniquement. - Valeurs par défaut : devise
SLE(USD et EUR aussi acceptés), fournisseurorange_money_sl. - Montants en chaînes à 2 décimales (
"150.00"), enintou enDecimal. Lesfloatsont refusés. Idempotency-Keyajouté automatiquement à chaque POST.- Vérification de la signature des webhooks (
X-Soamtar-Signature).
Pas encore sur PyPI
Tant que le paquet n'est pas publié, installez-le depuis le dépôt du SDK (pip install <chemin-ou-url-git>).
Installation
pip install soamtarConfiguration
from soamtar import SoamtarClient
client = SoamtarClient() # lit SOAMTAR_SECRET_KEY
client = SoamtarClient("sk_test_...") # ou passez la clé secrète
client = SoamtarClient(timeout=30) # secondes (15 par défaut)
client = SoamtarClient(sign_requests=True) # signature facultative des requêtes| Argument | Défaut | Remarques |
|---|---|---|
secret_key | SOAMTAR_SECRET_KEY | Doit commencer par sk_test_ ou sk_live_. |
base_url | https://app.so-amtar.com/v1 | Lu dans SOAMTAR_API_URL, puis SOAMTAR_BASE_URL (ancien nom, déprécié). |
timeout | 15 | En secondes. |
sign_requests | False | Ajoute Soamtar-Timestamp / Soamtar-Signature à chaque POST (voir Signature des requêtes). |
Utilisez une clé sk_test_… pendant l'intégration (simulation sur le même hôte, sans argent réel) et sk_live_… en production. Une clé secrète ne doit jamais se retrouver dans un navigateur ou une application mobile. client.is_test_mode() indique quelle clé est chargée.
Signature des requêtes
SoamtarClient(sign_requests=True) ajoute Soamtar-Timestamp: <unix> et Soamtar-Signature: v1=hex(HMAC_SHA256(secret_key, "<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
Le client reçoit une demande de paiement Orange Money sur son téléphone et la valide avec son code PIN.
payment = client.payments.create({
"merchant_ref": "ORDER-1042",
"amount": "150.00", # Le 150.00
"phone": "+23276000001", # E.164 : +232 suivi de 8 chiffres
"description": "Commande n° 1042",
"callback_url": "https://shop.example.sl/webhooks/soamtar", # facultatif
}, idempotency_key="order-1042")
print(payment.data["id"], payment.data["status"]) # ex. "pending"
print(payment.payment_url()) # page de paiement hébergéePage de paiement hébergée (SOAMTAR demande lui-même le numéro) : omettez phone, passez success_url / failed_url et redirigez vers payment.payment_url(). Sous Django, payment.to_django_redirect() renvoie directement la redirection.
callback_url (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).
Suivez le résultat par webhook, ou interrogez l'API :
client.payments.retrieve(payment.data["id"], wait=True) # attend un statut final (long polling)
client.payments.verify(payment.data["id"]) # revérifie auprès d'Orange Money
client.payments.list(status="success", per_page=50)
client.payments.status("TXN-…") # par identifiant ou référence SOAMTAR (pas votre merchant_ref)Envoyer de l'argent (payouts)
payout = client.payouts.create({
"merchant_ref": "PAYOUT-77",
"amount": "50.00",
"recipient": {"phone": "+23276000001", "name": "Aminata Kamara"},
"description": "Paiement fournisseur",
}, idempotency_key="payout-PAYOUT-77")
client.payouts.retrieve(payout.data["id"])
client.payouts.list(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 = client.refunds.create({
"transaction_id": payment.data["id"], # identifiant (ou référence) SOAMTAR du paiement
"amount": "50.00", # omis : remboursement total du restant
"reason": "customer_request",
"description": "Article en rupture de stock",
}, idempotency_key="refund-order-1042-1")
print(refund.data["reference"], refund.data["status"]) # "REF-…", "processing"
client.refunds.retrieve(refund.data["reference"]) # identifiant ou référence
client.refunds.list(transaction_id=payment.data["id"], 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.
Solde
client.balance.retrieve()
# {"object": "balance", "currency": "SLE", "available": …, "pending": …, "reserved": …, "last_updated": …}Webhooks
Les notifications partent vers les endpoints webhook que vous avez enregistrés (tableau de bord ou client.webhooks.create), signées avec le secret de chaque endpoint : X-Soamtar-Signature: t=<unix>,v1=<hex> (plus v2= pendant une rotation du secret), HMAC-SHA256(secret, "<t>.<corps brut>"), tolérance de 300 s.
Webhook.construct_event(payload, signature_header, secret) vérifie le corps brut et renvoie l'événement décodé, ou lève SignatureException.
Django
import os
from django.http import HttpResponse
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_POST
from soamtar import Webhook, SignatureException
@csrf_exempt
@require_POST
def soamtar_webhook(request):
try:
event = Webhook.construct_event(
request.body, # corps BRUT
request.headers.get("X-Soamtar-Signature"),
os.environ["SOAMTAR_WEBHOOK_SECRET"],
)
except SignatureException:
return HttpResponse(status=400)
if event["event"] == "payment.success":
mark_order_paid(event["data"]["merchant_reference"])
elif event["event"] in ("payment.failed", "payment.cancelled", "payment.expired"):
mark_order_failed(event["data"]["merchant_reference"])
elif event["event"] == "refund.success":
mark_order_refunded(event["data"]["merchant_reference"], event["data"]["amount"])
# webhook.test et le reste : répondez simplement 200
return HttpResponse(status=200)FastAPI
Le SDK est synchrone (HTTP de la bibliothèque standard) : déclarez avec def les routes qui appellent l'API, FastAPI les exécutera dans son pool de threads.
import os
from fastapi import FastAPI, HTTPException, Request
from fastapi.responses import RedirectResponse
from soamtar import SoamtarClient, Webhook, SignatureException
app = FastAPI()
client = SoamtarClient()
@app.post("/checkout")
def checkout(order_id: str, amount: str):
payment = client.payments.create({
"merchant_ref": order_id,
"amount": amount,
"success_url": "https://shop.example.sl/merci",
"failed_url": "https://shop.example.sl/panier",
}, idempotency_key=f"order-{order_id}")
return RedirectResponse(payment.payment_url(), status_code=303)
@app.post("/webhooks/soamtar")
async def soamtar_webhook(request: Request):
try:
event = Webhook.construct_event(
await request.body(),
request.headers.get("X-Soamtar-Signature"),
os.environ["SOAMTAR_WEBHOOK_SECRET"],
)
except SignatureException:
raise HTTPException(status_code=400)
# traitez event["event"] / event["data"]
return {"received": True}Flask
import os
from flask import Flask, abort, request
from soamtar import Webhook, SignatureException
app = Flask(__name__)
@app.post("/webhooks/soamtar")
def soamtar_webhook():
try:
event = Webhook.construct_event(
request.get_data(),
request.headers.get("X-Soamtar-Signature"),
os.environ["SOAMTAR_WEBHOOK_SECRET"],
)
except SignatureException:
abort(400)
return "", 200Dédoublonnez sur event + data.id : un même événement peut arriver plusieurs fois.
É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.send_test() et par le bouton de test du tableau de bord, quels que soient vos abonnements. Voir Webhooks.
Gérer les endpoints
Avec une clé disposant du scope can_manage_webhooks :
client.webhooks.create("https://shop.example.sl/webhooks/soamtar") # par défaut : payment.success/failed, payout.success/failed
client.webhooks.create("https://shop.example.sl/webhooks/soamtar", ["payment.success", "refund.success", "refund.failed"]) # idempotent par URL, renvoie le secret
client.webhooks.list()
client.webhooks.lookup("https://shop.example.sl/webhooks/soamtar")
client.webhooks.secret(endpoint_id)
client.webhooks.send_test() # envoie un événement webhook.test à vos endpoints actifsAlias dépréciés, toujours fonctionnels : client.webhooks.test() (utilisez send_test()), client.status.retrieve() (utilisez client.payments.status()).
Erreurs
from soamtar import ApiException, SoamtarException
try:
client.payments.create({...})
except ApiException as e:
print(e.status_code, e.error_code, e.message, e.errors) # errors : messages par champ en 422
except SoamtarException as e:
print(e) # configuration, réseau ou saisie invalide (ex. montant en float)Réessayez une erreur 5xx ou réseau avec la même idempotency_key ; ne renvoyez jamais une 4xx telle quelle.
Tester votre webhook
import hashlib, hmac, json, time
from soamtar import Webhook
def sign(payload: str, secret: str) -> str:
t = int(time.time())
v1 = hmac.new(secret.encode(), f"{t}.{payload}".encode(), hashlib.sha256).hexdigest()
return f"t={t},v1={v1}"
def test_signature_valide():
body = json.dumps({"event": "payment.success", "data": {"id": "tx_1"}})
event = Webhook.construct_event(body, sign(body, "whsec_test"), "whsec_test")
assert event["event"] == "payment.success"