Developers
REST API reference for integrating SwahiliPay into your website, app, or back-office system.
SwahiliPay exposes a REST API for merchants integrating as sub-merchants on this aggregator. Base URL for API calls:
https://swahilipay.co.tz/api/v1
| Endpoint | Method | Purpose |
|---|---|---|
/payments |
POST | Record a collection or initiate mobile money (with phone in meta) |
/payments/{reference} |
GET | Look up payment status by your reference (or provider reference) |
/balance |
GET | Read wallet balance for the authenticated business |
As a merchant integrator you connect to SwahiliPay only. You do not need upstream gateway credentials — the platform handles mobile money routing for you.
| What you need | Where to get it | Used for |
|---|---|---|
API base URLhttps://swahilipay.co.tz/api/v1 |
This documentation / your dashboard | All API requests (payments, balance) |
API key — header X-Api-Key |
API keys | Identifies your business on every request |
API secret — header X-Api-Secret |
Shown once when you generate a key | Authenticates the request (like a password) |
| Webhook URL | Business profile | Your server URL where SwahiliPay sends payment events |
| Webhook secret | Business profile (set or rotate anytime) | Verify X-SwahiliPay-Signature on inbound webhooks |
Send these headers on every request. Create keys under API keys.
| Header | Required | Description |
|---|---|---|
X-Api-Key | Yes | Public API key |
X-Api-Secret | Yes | Secret shown once when the key is generated |
Content-Type | POST | application/json |
Idempotency-Key | Optional | Replay-safe retries for POST /payments |
Rate limit: 120 requests per minute per API key. Optional IP allowlist can be configured on the business profile.
curl -X GET "https://swahilipay.co.tz/api/v1/balance" \
-H "X-Api-Key: YOUR_API_KEY" \
-H "X-Api-Secret: YOUR_API_SECRET" \
-H "Accept: application/json"
Send JSON in the request body. Your API key already identifies the business — you do not pass business_id or gateway credentials in the body.
| Field | Type | Required | Description |
|---|---|---|---|
amount |
number | Yes | Collection amount. Minimum 0.01; mobile money minimum 500 TZS. |
currency |
string | No | Defaults to TZS. Mobile collections require TZS. |
reference |
string | No | Your unique payment reference (invoice ID, order number). Auto-generated if omitted. |
provider_reference |
string | No | Upstream gateway reference when returned by the payment provider (read-only in responses). |
meta |
object | No | Extra data. Include phone_number here to trigger a mobile money push. |
| Field | Type | Required | Description |
|---|---|---|---|
meta.phone_number |
string | Yes* | Customer mobile number. Format 255XXXXXXXXX (Tanzania, no +). Alias: meta.phone |
meta.payment_type |
string | No | Defaults to mobile |
meta.customer |
object | Recommended | Customer identity sent to the mobile money network |
meta.customer.firstname |
string | Recommended | Customer first name (alias: first_name) |
meta.customer.lastname |
string | Recommended | Customer last name (alias: last_name) |
meta.customer.email |
string | Recommended | Customer email |
meta.return_url |
string (URL) | No | Card only — your HTTPS (or app deep-link) page after checkout. See Card return & cancel URLs. |
meta.cancel_url |
string (URL) | No | Card only — your page if the customer cancels. Falls back to return_url. |
* Required only when you want a USSD / mobile-money push to the customer phone.
{
"amount": 1500,
"currency": "TZS",
"reference": "INV-1001",
"meta": {
"phone_number": "255718666752",
"payment_type": "mobile",
"customer": {
"firstname": "John",
"lastname": "Doe",
"email": "john@example.com"
}
}
}
| Field | Description |
|---|---|
id | SwahiliPay payment ID |
reference | Your reference (or the auto-generated one) |
amount / currency | Recorded collection amount |
status | pending (awaiting customer on phone), completed, or failed |
collection_type | mobile or card |
mobile_push | true when a wallet prompt was sent |
provider_reference | Upstream order/reference when available |
message | Human-readable status hint |
Use this when you want the customer to receive a mobile money prompt on their phone. Include
meta.phone_number and customer details in the request below.
255XXXXXXXXX (Tanzania, no leading +)TZSfirstname, lastname, email in meta.customerstatus: pending until the customer confirms on their phone. Balance is credited when payment moves to completed.
Your payment.collected webhook fires after completion.
If the customer declines or the attempt fails, you receive payment.failed.
curl -X POST "https://swahilipay.co.tz/api/v1/payments" \
-H "X-Api-Key: YOUR_API_KEY" \
-H "X-Api-Secret: YOUR_API_SECRET" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-H "Idempotency-Key: INV-1001" \
-d '{
"amount": 1500,
"currency": "TZS",
"reference": "INV-1001",
"meta": {
"phone_number": "255718666752",
"payment_type": "mobile",
"customer": {
"firstname": "John",
"lastname": "Doe",
"email": "john@example.com"
}
}
}'
{
"id": 42,
"reference": "INV-1001",
"amount": "1500.00",
"currency": "TZS",
"status": "pending",
"collection_type": "mobile",
"mobile_push": true,
"provider_reference": "gateway-ref-when-available",
"message": "Mobile collection initiated. Awaiting customer confirmation on phone."
}
Same Idempotency-Key for the same business returns the existing payment with "idempotent_replay": true.
Use this when the customer should pay by debit or credit card on a hosted checkout page. Set
meta.payment_method to card.
The API returns payment_url — redirect the customer to that URL to enter card details.
meta.payment_method: cardmeta.phone_number: requiredmeta.customer.email: requiredmeta.customer.firstname / lastname: recommendedmeta.billing: optional — address, city, postcode; defaults to Tanzania if omittedmeta.return_url: optional — your HTTPS page after success (see below)meta.cancel_url: optional — your HTTPS page if the customer cancels (see below)TZScurl -X POST "https://swahilipay.co.tz/api/v1/payments" \
-H "X-Api-Key: YOUR_API_KEY" \
-H "X-Api-Secret: YOUR_API_SECRET" \
-H "Content-Type: application/json" \
-d '{
"amount": 1500,
"currency": "TZS",
"reference": "INV-CARD-1001",
"meta": {
"payment_method": "card",
"phone_number": "255718666752",
"customer": {
"firstname": "John",
"lastname": "Doe",
"email": "john@example.com"
},
"return_url": "https://your-site.com/payments/success",
"cancel_url": "https://your-site.com/payments/cancel"
}
}'
{
"id": 43,
"reference": "INV-CARD-1001",
"amount": "1500.00",
"currency": "TZS",
"status": "pending",
"collection_type": "card",
"mobile_push": false,
"payment_url": "https://checkout.example/pay/...",
"message": "Card checkout ready. Redirect the customer to payment_url to pay with Visa or Mastercard."
}
Balance is credited when the card payment completes and your payment.collected webhook fires. Cancellations, declines, and expired checkouts send payment.failed with status: failed.
After the customer finishes (or cancels) on the card checkout page, the payment gateway always sends them back to SwahiliPay first, for example:
https://swahilipay.co.tz/payment/selcom/redirect?order_id=ORD-...
https://swahilipay.co.tz/payment/selcom/cancel?order_id=ORD-...
What you configure on your side:
/payments/success and /payments/cancel.meta.return_url and meta.cancel_url.| Field | When to use |
|---|---|
meta.return_url |
Customer completed checkout (success or still confirming). SwahiliPay redirects here after the platform return handler runs. |
meta.cancel_url |
Customer cancelled on the card page. If omitted, return_url is used as a fallback for cancel. |
Query parameters appended to your URL:
| Parameter | Meaning |
|---|---|
reference | Your payment reference |
status | pending, completed, or failed |
amount | Payment amount |
currency | Usually TZS |
cancelled | 1 if the customer cancelled, otherwise 0 |
https://your-site.com/payments/success?reference=INV-CARD-1001&status=pending&amount=1500.00¤cy=TZS&cancelled=0
https://your-site.com/payments/cancel?reference=INV-CARD-1001&status=failed&amount=1500.00¤cy=TZS&cancelled=1
GET /payments/{reference} or wait for a payment.collected or payment.failed webhook.status may still be pending until the card webhook completes — do not credit the customer from the redirect alone.cancelled=1 and status=failed. The matching webhook is payment.failed with failure_reason: cancelled — there is no separate cancelled payment status.If a webhook is delayed or missed, poll this endpoint as a fallback. Use your payment reference from POST /payments. You may also look up by provider_reference.
curl -X GET "https://swahilipay.co.tz/api/v1/payments/INV-1001" \
-H "X-Api-Key: YOUR_API_KEY" \
-H "X-Api-Secret: YOUR_API_SECRET" \
-H "Accept: application/json"
{
"id": 42,
"reference": "INV-1001",
"provider_reference": "ORD-ABC123",
"amount": "1500.00",
"currency": "TZS",
"status": "completed",
"collection_type": "card",
"created_at": "2026-05-18T10:30:00.000000Z",
"updated_at": "2026-05-18T10:31:12.000000Z"
}
Terminal statuses:
| status | Meaning |
|---|---|
pending | Customer has not finished (USSD prompt or card page still open). Keep polling or wait for a webhook. |
completed | Paid. Credit the customer. Matches webhook payment.collected. |
failed | Declined, cancelled, or expired. Do not credit. Matches webhook payment.failed. When present, failure_reason is cancelled, expired, or a gateway failure. |
{
"id": 43,
"reference": "INV-CARD-1001",
"status": "failed",
"collection_type": "card",
"failure_reason": "cancelled"
}
curl -X GET "https://swahilipay.co.tz/api/v1/balance" \
-H "X-Api-Key: YOUR_API_KEY" \
-H "X-Api-Secret: YOUR_API_SECRET" \
-H "Accept: application/json"
{
"business": { "name": "Acme Ltd", "slug": "acme-ltd" },
"balance": "45000.00",
"reserved_balance": "5000.00",
"available_balance": "40000.00",
"currency": "TZS"
}
Configure your webhook URL and signing secret on Business profile. When a payment is recorded, SwahiliPay POSTs JSON to your endpoint.
Events: payment.collected (status completed) and payment.failed (status failed — declined, cancelled, or expired). Ignore unknown event names so future types do not break your endpoint.
Your endpoint must:
Content-Type: application/jsonX-SwahiliPay-Signature header to confirm the request is authenticOn your Business profile, find the Webhook signing secret field and click the vpn_key generate button. A whsec_... value will be created for you. Copy it — you will need it to verify signatures.
Enter your public HTTPS endpoint URL in the Webhook URL field, then save. Example:
https://yourapp.example/api/swahilipay/webhook
| Header | Value |
|---|---|
Content-Type | application/json |
X-SwahiliPay-Event | payment.collected or payment.failed |
X-SwahiliPay-Signature | HMAC-SHA256 hex digest of the raw JSON body, keyed with your webhook secret |
{
"event": "payment.collected",
"data": {
"id": 42,
"reference": "INV-1001",
"amount": "1500.00",
"currency": "TZS",
"status": "completed",
"collection_type": "mobile",
"provider_reference": "gw-ref-abc123",
"created_at": "2026-05-18T10:30:00+00:00",
"updated_at": "2026-05-18T10:31:12+00:00"
}
}
{
"event": "payment.failed",
"data": {
"id": 43,
"reference": "INV-CARD-1001",
"amount": "1500.00",
"currency": "TZS",
"status": "failed",
"collection_type": "card",
"failure_reason": "cancelled",
"created_at": "2026-05-18T10:30:00+00:00",
"updated_at": "2026-05-18T10:32:00+00:00"
}
}
| Field | Description |
|---|---|
event | payment.collected or payment.failed |
data.id | SwahiliPay internal payment ID |
data.reference | Your unique reference (or auto-generated) |
data.amount | Decimal string of the collected amount |
data.currency | Currency code (e.g. TZS) |
data.status | completed or failed — matches the event. Cancelled checkouts use failed, not a separate cancelled status. |
data.collection_type | mobile or card |
data.provider_reference | Upstream gateway reference (if available) |
data.failure_reason | On failed events: cancelled, expired, or a gateway reason |
data.created_at | ISO 8601 timestamp |
$raw = file_get_contents('php://input');
$signature = $_SERVER['HTTP_X_SWAHILIPAY_SIGNATURE'] ?? '';
$secret = 'whsec_...'; // from your Business Profile
$expected = hash_hmac('sha256', $raw, $secret);
if (!hash_equals($expected, $signature)) {
http_response_code(401);
exit('Invalid signature');
}
$payload = json_decode($raw, true);
error_log('Payment event: ' . $payload['event'] . ' ' . $payload['data']['reference']);
http_response_code(200);
echo 'ok';
const crypto = require('crypto');
app.post('/api/swahilipay/webhook', express.raw({ type: 'application/json' }), (req, res) => {
const raw = req.body;
const signature = req.headers['x-swahilipay-signature'] || '';
const secret = 'whsec_...'; // from your Business Profile
const expected = crypto.createHmac('sha256', secret).update(raw).digest('hex');
if (!crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature))) {
return res.status(401).json({ error: 'Invalid signature' });
}
const payload = JSON.parse(raw);
console.log('Payment event:', payload.event, payload.data.reference);
res.status(200).json({ ok: true });
});
import hmac, hashlib
from flask import Flask, request, jsonify
app = Flask(__name__)
WEBHOOK_SECRET = 'whsec_...' # from your Business Profile
@app.route('/api/swahilipay/webhook', methods=['POST'])
def webhook():
raw = request.get_data()
signature = request.headers.get('X-SwahiliPay-Signature', '')
expected = hmac.new(WEBHOOK_SECRET.encode(), raw, hashlib.sha256).hexdigest()
if not hmac.compare_digest(expected, signature):
return jsonify(error='Invalid signature'), 401
payload = request.get_json()
print('Payment event:', payload['event'], payload['data']['reference'])
return jsonify(ok=True), 200
Simulate a webhook delivery from the command line to verify your endpoint works. Replace the secret and URL with your own.
SECRET="whsec_..."
WEBHOOK_URL="https://yourapp.example/api/swahilipay/webhook"
BODY='{"event":"payment.collected","data":{"id":42,"reference":"INV-1001","amount":"1500.00","currency":"TZS","status":"completed","created_at":"2026-05-18T10:30:00+00:00"}}'
SIGNATURE=$(printf '%s' "$BODY" | openssl dgst -sha256 -hmac "$SECRET" | awk '{print $NF}')
curl -X POST "$WEBHOOK_URL" \
-H "Content-Type: application/json" \
-H "X-SwahiliPay-Event: payment.collected" \
-H "X-SwahiliPay-Signature: $SIGNATURE" \
-d "$BODY"
To rotate your webhook secret without downtime:
| Behaviour | Detail |
|---|---|
| Timeout | 15 seconds |
| Retries on failure | 3 attempts total |
| Expected response | HTTP 2xx |
| On final failure | Delivery is abandoned; payment is not affected |
X-SwahiliPay-Signature before processing any webhook. Never trust the payload without verification.
Store your webhook secret securely (e.g. environment variable) — do not commit it to source control.
| Code | Typical cause |
|---|---|
401 | Missing or invalid API key / secret |
403 | Business suspended, sanctions match, or IP not on allowlist |
422 | KYC required, limits exceeded, duplicate reference, inactive business |
200 | Idempotency replay (existing payment) |
201 | New payment created |
Error bodies use a message string. Validation errors (422) may also include an errors object keyed by field name.
{
"message": "Invalid API credentials."
}
{
"message": "The amount field is required.",
"errors": {
"amount": ["The amount field is required."]
}
}
https://swahilipay.co.tz/api/v1/payments with meta.phone_number for mobile money, or meta.payment_method: card for Visa/Mastercard checkout.X-Api-Key, X-Api-Secret, and optional Idempotency-Key on every request.status: completed (webhook payment.collected) before treating funds as settled. Use GET /payments/{reference} if no webhook arrives.X-SwahiliPay-Signature.reference per payment.