SDK Python
Official Python client for the SOAMTAR API: collect payments and send payouts with Orange Money Sierra Leone, in leones (SLE).
- Package:
soamtar(version 1.1.0). Python 3.8+, standard library only. - Defaults: currency
SLE(USD and EUR also accepted), providerorange_money_sl. - Amounts as 2-decimal strings (
"150.00"), anintor aDecimal. Floats are refused. - Automatic
Idempotency-Keyon every POST. - Webhook signature verification (
X-Soamtar-Signature).
Not yet on PyPI
Until the package is published, install it from the SDK repository (pip install <path-or-git-url>).
Installation
pip install soamtarConfiguration
from soamtar import SoamtarClient
client = SoamtarClient() # reads SOAMTAR_SECRET_KEY
client = SoamtarClient("sk_test_...") # or pass the secret key
client = SoamtarClient(timeout=30) # seconds (default 15)
client = SoamtarClient(sign_requests=True) # optional request signing| Argument | Default | Notes |
|---|---|---|
secret_key | SOAMTAR_SECRET_KEY | Must start with sk_test_ or sk_live_. |
base_url | https://app.so-amtar.com/v1 | Read from SOAMTAR_API_URL, then SOAMTAR_BASE_URL (deprecated name). |
timeout | 15 | Seconds. |
sign_requests | False | Adds Soamtar-Timestamp / Soamtar-Signature to every POST (see Request signing). |
Use sk_test_… keys while integrating (simulation on the same host, no real money) and sk_live_… in production. Never put a secret key in a browser or mobile app. client.is_test_mode() tells you which one is loaded.
Request signing
SoamtarClient(sign_requests=True) adds Soamtar-Timestamp: <unix> and Soamtar-Signature: v1=hex(HMAC_SHA256(secret_key, "<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
The customer receives an Orange Money payment request on their phone and confirms with their PIN.
payment = client.payments.create({
"merchant_ref": "ORDER-1042",
"amount": "150.00", # Le 150.00
"phone": "+23276123456", # E.164: +232 followed by 8 digits
"description": "Order #1042",
"callback_url": "https://shop.example.sl/webhooks/soamtar", # optional
}, idempotency_key="order-1042")
print(payment.data["id"], payment.data["status"]) # e.g. "pending"
print(payment.payment_url()) # hosted checkout pageHosted checkout (SOAMTAR collects the phone number): leave out phone, pass success_url / failed_url and redirect to payment.payment_url(). In Django, payment.to_django_redirect() returns the redirect for you.
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).
Follow the outcome with the webhook, or poll:
client.payments.retrieve(payment.data["id"], wait=True) # long-polls until a final status
client.payments.verify(payment.data["id"]) # re-checks with Orange Money
client.payments.list(status="success", per_page=50)
client.payments.status("TXN-…") # by SOAMTAR id or reference (not your merchant_ref)Send money (payouts)
payout = client.payouts.create({
"merchant_ref": "PAYOUT-77",
"amount": "50.00",
"recipient": {"phone": "+23276123456", "name": "Aminata Kamara"},
"description": "Supplier payment",
})
client.payouts.retrieve(payout.data["id"])
client.payouts.list(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 = client.refunds.create({
"transaction_id": payment.data["id"], # SOAMTAR id (or reference) of the payment
"amount": "50.00", # omit for a full refund of what remains
"reason": "customer_request",
"description": "Item out of stock",
}, idempotency_key="refund-order-1042-1")
print(refund.data["reference"], refund.data["status"]) # "REF-…", "processing"
client.refunds.retrieve(refund.data["reference"]) # id or reference
client.refunds.list(transaction_id=payment.data["id"], 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.
Balance
client.balance.retrieve()
# {"object": "balance", "currency": "SLE", "available": …, "pending": …, "reserved": …, "last_updated": …}Webhooks
Notifications are sent to the webhook endpoints you register (dashboard or client.webhooks.create), signed with that endpoint's secret: X-Soamtar-Signature: t=<unix>,v1=<hex> (plus v2= during a secret rotation), HMAC-SHA256(secret, "<t>.<raw body>"), 300 s tolerance.
Webhook.construct_event(payload, signature_header, secret) verifies the raw body and returns the decoded event, or raises 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, # RAW body
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 and anything else: just answer 200
return HttpResponse(status=200)FastAPI
The SDK is synchronous (standard library HTTP): declare the routes that call the API with def so FastAPI runs them in its thread pool.
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/thanks",
"failed_url": "https://shop.example.sl/cart",
}, 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)
# handle 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 "", 200Deduplicate on event + data.id: the same event may be delivered more than once.
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.send_test() and the dashboard's test button whatever your subscriptions. See Webhooks.
Manage endpoints
With a key that has the can_manage_webhooks scope:
client.webhooks.create("https://shop.example.sl/webhooks/soamtar") # defaults: payment.success/failed, payout.success/failed
client.webhooks.create("https://shop.example.sl/webhooks/soamtar", ["payment.success", "refund.success", "refund.failed"]) # idempotent by URL, returns the secret
client.webhooks.list()
client.webhooks.lookup("https://shop.example.sl/webhooks/soamtar")
client.webhooks.secret(endpoint_id)
client.webhooks.send_test() # sends a webhook.test event to your active endpointsDeprecated aliases, still working: client.webhooks.test() (use send_test()), client.status.retrieve() (use client.payments.status()).
Errors
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: field messages on 422
except SoamtarException as e:
print(e) # configuration, network or invalid input (e.g. a float amount)Retry a 5xx or a network error with the same idempotency_key; never retry a 4xx unchanged.
Testing your webhook handler
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_valid_signature():
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"