Developers

Developer documentation

REST API reference for integrating SwahiliPay into your website, app, or back-office system.

Create a merchant account to issue API keys and test in sandbox before going live.

Get API access

Overview

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

Getting started

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 URL
https://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
You do not send: upstream gateway API keys, gateway secrets, vendor IDs, or gateway base URLs. Those are configured by the platform operator.

Authentication

Send these headers on every request. Create keys under API keys.

HeaderRequiredDescription
X-Api-KeyYesPublic API key
X-Api-SecretYesSecret shown once when the key is generated
Content-TypePOSTapplication/json
Idempotency-KeyOptionalReplay-safe retries for POST /payments

Rate limit: 120 requests per minute per API key. Optional IP allowlist can be configured on the business profile.

Minimal authenticated request

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"

POST /payments — request payload

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.

Top-level fields

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.

meta fields (mobile collection)

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.

Full JSON example — mobile collection

{
  "amount": 1500,
  "currency": "TZS",
  "reference": "INV-1001",
  "meta": {
    "phone_number": "255718666752",
    "payment_type": "mobile",
    "customer": {
      "firstname": "John",
      "lastname": "Doe",
      "email": "john@example.com"
    }
  }
}

Response fields

Field Description
idSwahiliPay payment ID
referenceYour reference (or the auto-generated one)
amount / currencyRecorded collection amount
statuspending (awaiting customer on phone), completed, or failed
collection_typemobile or card
mobile_pushtrue when a wallet prompt was sent
provider_referenceUpstream order/reference when available
messageHuman-readable status hint

Mobile collection (STK / USSD push)

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.

Request requirements
  • Phone: 255XXXXXXXXX (Tanzania, no leading +)
  • Amount: numeric, minimum 500 TZS
  • Currency: TZS
  • Customer: firstname, lastname, email in meta.customer
  • Networks: Airtel Money, M-Pesa, Mixx by Yas, Halotel
Important: Mobile collections return status: 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"
      }
    }
  }'

Success response (201) — mobile

{
  "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."
}

Idempotent replay (200)

Same Idempotency-Key for the same business returns the existing payment with "idempotent_replay": true.

Card collection (Visa / Mastercard)

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.

Requirements
  • meta.payment_method: card
  • meta.phone_number: required
  • meta.customer.email: required
  • meta.customer.firstname / lastname: recommended
  • meta.billing: optional — address, city, postcode; defaults to Tanzania if omitted
  • meta.return_url: optional — your HTTPS page after success (see below)
  • meta.cancel_url: optional — your HTTPS page if the customer cancels (see below)
  • Currency: TZS
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" \
  -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"
    }
  }'

Success response (201) — card

{
  "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.

Card checkout timeout: Hosted card pages expire after 15 minutes if the customer does not finish paying. The payment then moves to failed with failure_reason expired, and a payment.failed webhook is sent. Closing the tab without pressing Cancel has the same outcome after that timeout.

Return & cancel URLs — what you configure

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-...
Do not replace those SwahiliPay URLs. They belong to the platform so we can update payment status before the customer leaves. You never put your own website as the Selcom redirect or cancel URL.

What you configure on your side:

  1. Create two HTTPS pages on your website (or deep links in your app), for example /payments/success and /payments/cancel.
  2. Pass them in the payment request as meta.return_url and meta.cancel_url.
  3. After SwahiliPay finishes processing, we redirect the customer to your URL with query parameters (see table below).
  4. If you omit both URLs, the customer stays on the SwahiliPay status page — that is fine for payment links.
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
referenceYour payment reference
statuspending, completed, or failed
amountPayment amount
currencyUsually TZS
cancelled1 if the customer cancelled, otherwise 0
https://your-site.com/payments/success?reference=INV-CARD-1001&status=pending&amount=1500.00&currency=TZS&cancelled=0
https://your-site.com/payments/cancel?reference=INV-CARD-1001&status=failed&amount=1500.00&currency=TZS&cancelled=1
Recommended on your end:
  • Treat the browser return as UX only — show “processing” or “cancelled”, then confirm with GET /payments/{reference} or wait for a payment.collected or payment.failed webhook.
  • On return, status may still be pending until the card webhook completes — do not credit the customer from the redirect alone.
  • If the customer cancelled, cancelled=1 and status=failed. The matching webhook is payment.failed with failure_reason: cancelled — there is no separate cancelled payment status.
  • Use HTTPS URLs you control. Do not send localhost or private IPs in production.

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

Success response (200)

{
  "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:

statusMeaning
pendingCustomer has not finished (USSD prompt or card page still open). Keep polling or wait for a webhook.
completedPaid. Credit the customer. Matches webhook payment.collected.
failedDeclined, 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"
}

Check balance

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

Outbound webhooks (merchant integrators)

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.

Step 1 — Set up your endpoint

Your endpoint must:

  1. Accept POST requests with Content-Type: application/json
  2. Respond with HTTP 2xx within 15 seconds
  3. Verify the X-SwahiliPay-Signature header to confirm the request is authentic

Step 2 — Generate your signing secret

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

Step 3 — Configure the webhook URL

Enter your public HTTPS endpoint URL in the Webhook URL field, then save. Example:

https://yourapp.example/api/swahilipay/webhook

Headers sent on every webhook

HeaderValue
Content-Typeapplication/json
X-SwahiliPay-Eventpayment.collected or payment.failed
X-SwahiliPay-SignatureHMAC-SHA256 hex digest of the raw JSON body, keyed with your webhook secret

Webhook payload

{
  "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"
  }
}
FieldDescription
eventpayment.collected or payment.failed
data.idSwahiliPay internal payment ID
data.referenceYour unique reference (or auto-generated)
data.amountDecimal string of the collected amount
data.currencyCurrency code (e.g. TZS)
data.statuscompleted or failed — matches the event. Cancelled checkouts use failed, not a separate cancelled status.
data.collection_typemobile or card
data.provider_referenceUpstream gateway reference (if available)
data.failure_reasonOn failed events: cancelled, expired, or a gateway reason
data.created_atISO 8601 timestamp

Verify signature — PHP

$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';

Verify signature — Node.js

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 });
});

Verify signature — Python

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

Test your endpoint with curl

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"

Rotating your secret

To rotate your webhook secret without downtime:

  1. Generate a new secret on the Business profile (old one still works in your code for now)
  2. Update your endpoint to use the new secret
  3. Deploy and verify the new secret works (use the curl test above)
  4. Save the Business profile again with the new secret to finalize

Delivery & retries

BehaviourDetail
Timeout15 seconds
Retries on failure3 attempts total
Expected responseHTTP 2xx
On final failureDelivery is abandoned; payment is not affected
Security: Always verify 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.

HTTP errors

CodeTypical cause
401Missing or invalid API key / secret
403Business suspended, sanctions match, or IP not on allowlist
422KYC required, limits exceeded, duplicate reference, inactive business
200Idempotency replay (existing payment)
201New 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."]
  }
}

Integration checklist

  1. Complete business KYC if required on this environment.
  2. Note your API base URL: https://swahilipay.co.tz/api/v1
  3. Generate API key + secret under API keys; store the secret securely.
  4. Set webhook URL and signing secret on the business profile.
  5. Optional: configure API IP allowlist on the business profile.
  6. POST /payments with meta.phone_number for mobile money, or meta.payment_method: card for Visa/Mastercard checkout.
  7. Send headers X-Api-Key, X-Api-Secret, and optional Idempotency-Key on every request.
  8. Wait for status: completed (webhook payment.collected) before treating funds as settled. Use GET /payments/{reference} if no webhook arrives.
  9. Implement webhook verification with X-SwahiliPay-Signature.
  10. Reconcile using your unique reference per payment.