Skip to content

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), provider orange_money_sl.
  • Amounts as 2-decimal strings ("150.00"), an int or a Decimal. Floats are refused.
  • Automatic Idempotency-Key on 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 ​

bash
pip install soamtar

Configuration ​

python
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
ArgumentDefaultNotes
secret_keySOAMTAR_SECRET_KEYMust start with sk_test_ or sk_live_.
base_urlhttps://app.so-amtar.com/v1Read from SOAMTAR_API_URL, then SOAMTAR_BASE_URL (deprecated name).
timeout15Seconds.
sign_requestsFalseAdds 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.

python
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 page

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

python
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) ​

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

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

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

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

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

python
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 "", 200

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

python
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 endpoints

Deprecated aliases, still working: client.webhooks.test() (use send_test()), client.status.retrieve() (use client.payments.status()).

Errors ​

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

python
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"

SOAMTAR Payment API Documentation.