API Documentation
Everything you need to integrate Samsoftpay into your application.
Authentication
Samsoftpay uses Bearer token authentication. Pass your Secret Key in the Authorization header on every request.
import requests
headers = {
"Authorization": "Bearer sk_live_YOUR_SECRET_KEY",
"Content-Type": "application/json",
}
const headers = {
"Authorization": "Bearer sk_live_YOUR_SECRET_KEY",
"Content-Type": "application/json",
};
curl -H "Authorization: Bearer sk_live_YOUR_SECRET_KEY" \
https://yourapp.onrender.com/v1/charges
Required Headers
All POST requests require these additional headers:
| Header | Description |
|---|---|
Idempotency-Key required |
A unique UUID per request. Send the same key to safely retry without double-charging. Generate with uuid.uuid4() or crypto.randomUUID(). |
X-Timestamp required |
Current Unix timestamp in seconds (int(time.time())). Requests older than 5 minutes are rejected to prevent replay attacks. |
import time, uuid
headers = {
"Authorization": "Bearer sk_live_YOUR_SECRET_KEY",
"Idempotency-Key": str(uuid.uuid4()),
"X-Timestamp": str(int(time.time())),
"Content-Type": "application/json",
}
Charges
Collect money from a customer via mobile money or card.
Create a Charge
| Parameter | Type | Description |
|---|---|---|
amount required | integer | Amount in UGX (minor units). Must be > 0. |
currency optional | string | Currently only "UGX". Default: "UGX". |
channel required | string | "mtn_momo", "airtel_money", or "card". |
customer.phone required | string | Customer's phone number. E.g. "256700123456". |
reference optional | string | Your internal order/reference ID. |
import requests, time, uuid
resp = requests.post(
"https://api.samsoftpay.com/v1/charges",
headers={
"Authorization": "Bearer sk_live_YOUR_SECRET_KEY",
"Idempotency-Key": str(uuid.uuid4()),
"X-Timestamp": str(int(time.time())),
"Content-Type": "application/json",
},
json={
"amount": 10000,
"currency": "UGX",
"channel": "mtn_momo",
"customer": {"phone": "256700123456"},
"reference": "order-001",
}
)
print(resp.json())
# {"id": "txn_abc123", "status": "authorized", "amount": 10000, "fee": 200, ...}
const resp = await fetch("https://api.samsoftpay.com/v1/charges", {
method: "POST",
headers: {
"Authorization": "Bearer sk_live_YOUR_SECRET_KEY",
"Idempotency-Key": crypto.randomUUID(),
"X-Timestamp": String(Math.floor(Date.now() / 1000)),
"Content-Type": "application/json",
},
body: JSON.stringify({
amount: 10000,
currency: "UGX",
channel: "mtn_momo",
customer: { phone: "256700123456" },
reference: "order-001",
}),
});
const data = await resp.json();
console.log(data); // {id: "txn_abc123", status: "authorized", ...}
curl -X POST https://api.samsoftpay.com/v1/charges \
-H "Authorization: Bearer sk_live_YOUR_SECRET_KEY" \
-H "Idempotency-Key: $(python3 -c 'import uuid; print(uuid.uuid4())')" \
-H "X-Timestamp: $(date +%s)" \
-H "Content-Type: application/json" \
-d '{"amount":10000,"currency":"UGX","channel":"mtn_momo","customer":{"phone":"256700123456"}}'
1.5% fee (min UGX 200, cap UGX 5,000) is automatically calculated and returned in the fee field. The merchant receives amount - fee.Get a Charge
resp = requests.get(
"https://api.samsoftpay.com/v1/charges/txn_abc123",
headers={"Authorization": "Bearer sk_live_YOUR_SECRET_KEY"}
)
# status: "pending" | "authorized" | "succeeded" | "failed"
Payouts
Send money out to a recipient's mobile money wallet. The merchant must have sufficient available balance.
Create a Payout
| Parameter | Type | Description |
|---|---|---|
amount required | integer | Amount in UGX to send. |
channel optional | string | "mtn_momo" (default). |
recipient.phone required | string | Recipient's phone number. |
recipient.name optional | string | Recipient's display name. |
resp = requests.post(
"https://api.samsoftpay.com/v1/payouts",
headers={
"Authorization": "Bearer sk_live_YOUR_SECRET_KEY",
"Idempotency-Key": str(uuid.uuid4()),
"X-Timestamp": str(int(time.time())),
"Content-Type": "application/json",
},
json={
"amount": 50000,
"currency": "UGX",
"channel": "mtn_momo",
"recipient": {"phone": "256780000001", "name": "Jane Doe"},
}
)
print(resp.json())
# {"id": "pout_xyz789", "status": "authorized", "fee": 750, ...}
const resp = await fetch("https://api.samsoftpay.com/v1/payouts", {
method: "POST",
headers: {
"Authorization": "Bearer sk_live_YOUR_SECRET_KEY",
"Idempotency-Key": crypto.randomUUID(),
"X-Timestamp": String(Math.floor(Date.now() / 1000)),
"Content-Type": "application/json",
},
body: JSON.stringify({
amount: 50000,
currency: "UGX",
channel: "mtn_momo",
recipient: { phone: "256780000001", name: "Jane Doe" },
}),
});
Payment Links
Generate a shareable URL your customers open to pay — no integration required on their end. Perfect for WhatsApp, SMS, and email.
| Parameter | Type | Description |
|---|---|---|
amount required | integer | Amount in UGX. |
description optional | string | Shown to the customer on the payment page. |
success_url optional | string | Redirect URL after successful payment. |
allow_multiple_uses optional | boolean | If true, the link can be paid more than once. Default: false. |
resp = requests.post(
"https://api.samsoftpay.com/v1/payment-links",
headers={
"Authorization": "Bearer sk_live_YOUR_SECRET_KEY",
"Content-Type": "application/json",
},
json={
"amount": 25000,
"description": "School Fees Payment",
"success_url": "https://yourapp.com/thank-you",
}
)
data = resp.json()
print(data["url"]) # Share this link with your customer
Webhook Events
Samsoftpay POSTs a JSON payload to your webhook_url whenever a charge or payout changes state. We retry up to 8 times with exponential backoff.
Respond with any 2xx status code within 5 seconds to acknowledge receipt.
Charge Events
{
"event": "charge.succeeded",
"data": {
"id": "txn_abc123",
"amount": 10000,
"fee": 200,
"currency": "UGX",
"channel": "mtn_momo",
"status": "succeeded",
"merchant_reference": "order-001",
"completed_at": "2024-01-15T10:30:00+00:00"
}
}
Possible event values: charge.succeeded, charge.failed.
Verifying Webhooks
Every request includes an X-Samsoftpay-Signature header — an HMAC-SHA256 of the raw request body signed with your WEBHOOK_SIGNING_SECRET. Always verify it before processing.
import hmac, hashlib
from flask import request, abort
WEBHOOK_SECRET = "your_webhook_signing_secret"
@app.post("/webhooks/samsoftpay")
def handle_webhook():
sig = request.headers.get("X-Samsoftpay-Signature", "")
expected = hmac.new(
WEBHOOK_SECRET.encode(),
request.data,
hashlib.sha256
).hexdigest()
if not hmac.compare_digest(expected, sig):
abort(400, "invalid signature")
event = request.get_json()
if event["event"] == "charge.succeeded":
order_id = event["data"]["merchant_reference"]
# mark order as paid in your database
pass
return {"ok": True}
const crypto = require("crypto");
app.post("/webhooks/samsoftpay", express.raw({ type: "application/json" }), (req, res) => {
const sig = req.headers["x-samsoftpay-signature"];
const expected = crypto
.createHmac("sha256", process.env.WEBHOOK_SECRET)
.update(req.body)
.digest("hex");
if (!crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(sig))) {
return res.status(400).send("invalid signature");
}
const event = JSON.parse(req.body);
if (event.event === "charge.succeeded") {
// mark order as paid
}
res.json({ ok: true });
});
Errors
All errors return JSON with an error field.
| Status | Meaning |
|---|---|
400 | Bad request — missing field, invalid value, or stale X-Timestamp. |
401 | Unauthorized — missing or invalid Bearer token. |
404 | Resource not found or belongs to a different merchant. |
409 | Idempotency key reused with a different request body. |
429 | Rate limit exceeded. Charges: 30/min. Payouts: 10/min. |
# Error response shape
{"error": "insufficient available balance: have 5000, need 50750 (amount 50000 + fee 750)"}