Integration Guide
Connect your application with Payme Africa using our secure HMAC-authenticated REST API. Follow this guide to authenticate and process transactions.
Authentication
All API requests must be authenticated using a custom HMAC-SHA256 signature. Include these headers with every request.
| Header | Description |
|---|---|
Content-Type Required |
Must be application/json |
X-App-ID Required |
Your unique App Identifier (e.g., MOBILE_APP_01) |
X-Timestamp Required |
Current Unix timestamp (integer). Used for signature and replay attack protection. |
X-Signature Required |
Base64 encoded HMAC‑SHA256 hash of MinifiedJSONBody + Timestamp using your
App Secret. |
X-Sandbox Optional |
Set to 1 to run in sandbox mode. Transactions are simulated and balance is
isolated. |
Replay Protection: Requests with a timestamp older than 5 minutes will be automatically rejected. Always use the current Unix timestamp.
Rate Limits
Requests are throttled per client IP and, for transaction submission
specifically, also per X-App-ID. Limits apply independently and both can trigger a
429 response.
| Scope | Applies to | Limit | On repeated violation |
|---|---|---|---|
| Per IP | All /api/v1/* endpoints |
30 requests / minute | 429 until the window resets, no suspension |
Per X-App-ID |
POST /api/v1/transact only |
10 requests / 10 seconds | 3 violations within 2 minutes → that App ID is suspended for
15 minutes (every request returns 429 until it
expires) |
The per-App-ID limit exists because transact
is the one endpoint that triggers a real, synchronous call to the upstream mobile money provider for
every request. A burst of concurrent calls from one integration doesn't just throttle that
integration, it can degrade the upstream connection for every client on the platform, so it's
enforced more strictly and with a cooldown, not just a rolling window.
| Field | Description |
|---|---|
status |
"error" |
message |
Human-readable reason (rate-limited vs. temporarily suspended) |
retry_after |
Seconds until you may retry (present on the per-request rate-limit response; not on the suspension response, which lasts the full 15 minutes) |
Best practice: queue and serialize transact calls on your side
rather than firing them concurrently, and back off on a 429 instead of retrying
immediately. If your integration has a legitimate need for sustained throughput above these
limits, contact support to discuss a higher per-App-ID allowance.
Integration Flows
Understand the end-to-end process for the most common integration scenarios.
1. Mobile Collection (Push)
Initiate payment → Customer receives USSD prompt → Enters PIN → You receive webhook confirmation.
2. Disbursement (Payout)
Check balance → Lookup Recipient (optional) → Submit Payout → Funds delivered instantly.
3. Card Tokenization & Pay
Redirect to Hosted Checkout → Save Card → Receive Token → Charge Token later for one-tap payments.
4. VCN Lifecycle
Submit Details → Issue Card → Show Details (Secure) → User pays online → Funds deducted from your app balance.
Collection (Receive Money)
Initiate a USSD push payment to a customer's mobile wallet. The customer will receive a prompt on their handset to confirm payment.
| Parameter | Type | Description |
|---|---|---|
action Required |
string | Must be "collection" |
amount Required |
integer | Amount in TZS. Must be greater than 0. |
msisdn Required |
string | Customer's phone number in international format (e.g., 255700000000) |
reference Required |
string | Your unique transaction reference. Must be globally unique per client. |
callback_url Optional |
string | URL to receive webhook notifications for this specific transaction. |
{
"action": "collection",
"amount": 1000,
"msisdn": "255700000000",
"reference": "ORDER_101",
"callback_url": "https://yourapp.com/callback"
}
{
"status": "success",
"transaction_id": "901",
"payment_status": "PENDING",
"provider_response": {
"result": "SUCCESS",
"resultcode": "000",
"message": "Payment notification logged"
}
}
Disbursement (Send Money)
Send funds to various channels. Use the channel field to route the
disbursement to the correct network or bank.
Card Payments & Checkout
There are two ways to process card payments: using a Hosted Checkout (for new cards) or Stored Card Tokens (for one-tap payments).
Important: Card payments with no billing info will be rejected by Selcom. Ensure all billing_* fields are provided.
Hosted Checkout (New Cards)
Use the checkout action to receive a payment_gateway_url where the customer can enter their card details.
{
"action": "checkout",
"amount": 10000,
"reference": "INV-1001",
"billing_firstname": "John",
"billing_lastname": "Doe",
"billing_address_1": "969 Market Street",
"billing_city": "San Francisco",
"billing_state_or_region": "CA",
"billing_postcode_or_pobox": "94103",
"billing_country": "US",
"billing_phone": "255000000000",
"redirect_url": "https://yourapp.com/success",
"cancel_url": "https://yourapp.com/cancel"
}
Stored Card Payment (One-tap)
Use the card-payment action with a previously obtained card_token.
{
"action": "card-payment",
"amount": 5000,
"reference": "INV-202",
"card_token": "CT_987654321",
"buyer_userid": "user_123",
"gateway_buyer_uuid": "GB_uuid_001"
}
Card Management
List and delete stored card tokens for a user. This allows you to provide a "saved cards" experience in your checkout flow.
List Stored Cards
| Parameter | Description |
|---|---|
buyer_userid Required |
The internal ID of your customer. |
gateway_buyer_uuid Optional |
Specific UUID if known. |
{
"status": "success",
"data": [
{
"card_token": "CT_987654321",
"card_number": "411111XXXXXX1111",
"expiry": "12/26",
"card_type": "VISA"
}
]
}
Delete Card
Parameter in URL: id (The index or card token to remove)
Virtual Card Network (VCN)
Issue and manage secure virtual cards for your customers. These cards can be used for online payments worldwide.
Issue New VCN
{
"msisdn": "255700000000",
"email": "user@example.com",
"name": "John Doe"
}
Change VCN Status
| Field | Value |
|---|---|
status |
"BLOCK" or "UNBLOCK" |
Show Full Card Details
⚠️ Secure Endpoint: Only call when the user enters their PIN on your UI.
Get VCN Details & Balance
Mobile Wallet & Selcom Pesa
{
"action": "disbursement",
"amount": 5000,
"msisdn": "255700000000",
"channel": "CASHIN",
"reference": "PAYOUT_001"
}
{
"status": "success",
"transaction_id": "902",
"payment_status": "COMPLETED",
"financials": {
"system_profit": 55,
"provider_fee": 345,
"total_deductible": 5400
}
}
Bank Transfer (Qwiksend)
| Parameter | Type | Description |
|---|---|---|
action Required |
string | "disbursement" |
channel Required |
string | "BANK" for Qwiksend bank transfer |
amount Required |
integer | Amount in TZS |
msisdn Required |
string | Sender's phone number |
recipient_bank_code Required |
string | Bank code (e.g., CRDB). See Channels section. |
recipient_account Required |
string | Recipient's bank account number |
recipient_name Optional |
string | Recipient's full name |
remarks Optional |
string | Transaction memo or notes |
reference Required |
string | Your unique transaction reference |
{
"action": "disbursement",
"channel": "BANK",
"amount": 50000,
"msisdn": "255700000000",
"recipient_bank_code": "CRDB",
"recipient_account": "01J12345678900",
"recipient_name": "John Doe",
"reference": "SALARY_BATCH_01",
"remarks": "February Salary"
}
Name Lookup
Validate recipient details before sending funds. Supports wallet (mobile money), bank account, and Selcom Pesa lookups.
Wallet Lookup
{
"type": "wallet",
"channel": "vodacom",
"msisdn": "255754000000"
}
Bank Account Lookup
{
"type": "bank",
"bank_code": "CRDB",
"account_number": "01J12345678900"
}
Sample Response
{
"status": "success",
"name": "Jane Doe",
"original_data": { ... }
}
Transaction Query
Check the current status of a transaction using your internal reference. For pending transactions, the middleware will automatically attempt a live status check with the provider.
{
"reference": "ORDER_101"
}
{
"status": "SUCCESS",
"reference": "ORDER_101",
"amount": 1000,
"currency": "TZS",
"payment_status": "COMPLETED",
"created_at": "2024-01-15 10:30:00",
"provider_checked": true,
"provider_message": null
}
If payment_status is still PENDING, check provider_checked: true means we successfully reached the provider and the transaction is genuinely still pending; false means the live check itself failed (see provider_message) and you should retry shortly rather than treating it as confirmed.
Live provider checks are rate-limited per transaction: at most one every 30 seconds, and only for transactions created within the last hour. Outside those limits the stored status is returned with provider_checked: false. Older pending transactions are still resolved automatically by the provider callback and our background reconciliation, and you'll receive your callback_url webhook when they are, so there's no need to keep polling them.
Account Summary
Retrieve a financial summary for your application including total volume and transaction counts, with a breakdown by collections vs. disbursements the two have unrelated failure modes and shouldn't be read as one blended number.
{} // No body required
{
"status": "SUCCESS",
"data": {
"collections": {
"total_count": 150,
"total_volume": 3800000,
"success_count": 140,
"pending_count": 2,
"failed_count": 8,
"delivered_count": 148
},
"disbursements": { "...same shape" },
"account_balance": 3150000
}
}
account_balance is your current available app balance (successful collections minus successful/pending disbursements and fees), scoped to live or sandbox depending on whether the X-Sandbox header was sent. Collections and disbursements are always reported separately there is no combined total, since the two have unrelated failure modes.
success_count is the real completed-payment count (status = 'success') the same thing account_balance is computed from. delivered_count counts requests that left our system and were accepted by the provider for collections that means the payment prompt actually reached the customer's phone; for disbursements it means the provider processed the payout. Neither field is redefined by this endpoint; that distinction only affects how the admin/portal dashboards label a number for humans, not this data contract. Rate/percentage fields (delivery rate, completion rate) are temporarily removed from this response while their definitions are finalized.
Webhooks (Callbacks)
Payme Africa does not require pre-registration of webhooks. Instead, you must provide a callback_url in each /transact request. The gateway will send an asynchronous POST request to this URL when the transaction reaches a final state.
Status Descriptions
| Field | Value | Description |
|---|---|---|
result |
"SUCCESS" |
The final outcome of the transaction was successful. |
result |
"FAILED" |
The transaction failed at the provider or was cancelled. |
payment_status |
"PENDING" |
Awaiting customer action (PIN entry). |
payment_status |
"COMPLETED" |
Funds have been successfully transferred. |
payment_status |
"FAILED" |
The payment process stopped due to an error. |
Sample Webhook Payload
{
"transid": "7945454515",
"reference": "ORDER_101",
"result": "SUCCESS",
"payment_status": "COMPLETED",
"amount": "1000",
"msisdn": "255700000000"
}
Verifying Webhook Signatures
To ensure the webhook came from the middleware, you must
verify the X-Middleware-Signature header.
Algorithm:
Base64( HMAC_SHA256( Payload + X-Timestamp, Your_Shared_Secret ) )
$payload = file_get_contents('php://input');
$timestamp = $_SERVER['HTTP_X_TIMESTAMP'];
$signature = $_SERVER['HTTP_X_MIDDLEWARE_SIGNATURE'];
$expected = base64_encode(
hash_hmac('sha256', $payload . $timestamp, $secret, true)
);
if (hash_equals($signature, $expected)) {
// ✓ Verified process the webhook safely
}
Testing Webhooks: Use the Webhook Testing tool in the Developer Portal to fire a
simulated test_ping event to your registered URL without needing a real transaction.
Transaction Result Codes
Common result codes returned in API responses and webhook payloads.
| Code | Description | Action Required |
|---|---|---|
000 |
Success | Transaction completed successfully. Deliver value. |
021 |
Pending | Transaction initiated. Wait for callback or poll /api/v1/query. |
009 |
Failed | Generic failure. Check account status or entered details. |
052 |
Insufficient Balance | Customer has insufficient funds in their wallet. |
056 |
User Cancelled | Customer cancelled the USSD push request. |
INS_FUNDS |
Insufficient App Balance | Your middleware application balance is too low for this disbursement (fees included). |
SERVICE_DISABLED |
Service Not Enabled | The requested service (collection/disbursement) is disabled for your application. |
PROVIDER_TIMEOUT |
Outcome Unknown (HTTP 202) | The provider received the request but did not answer in time. The transaction is left PENDING and may still complete. Do not retry with a new reference: wait for your callback_url or poll /api/v1/query. |
PROVIDER_UNAVAILABLE |
Provider Unreachable (HTTP 503) | The request never reached the provider and the transaction is FAILED. Safe to retry shortly with a new reference. |
OUT_OF_RANGE |
Amount Out of Range | The requested amount is outside the min/max allowed for this service tier. |
Supported Channels & Banks
Use these codes in the channel and recipient_bank_code
request fields.
Mobile Networks
| Channel Code | Network / Provider |
|---|---|
CASHIN |
Universal Any carrier (auto-detected) |
vodacom |
M-Pesa (Vodacom) |
airtel |
Airtel Money |
tigo |
Tigo Pesa (MIC Tanzania) |
halotel |
HaloPesa |
zantel |
EzyPesa (Zantel) |
ttcl |
T-Pesa (TTCL) |
SELCOM_PESA |
Selcom Pesa |
Popular Banks (Qwiksend)
| Bank Code | Bank Name |
|---|---|
CRDB |
CRDB Bank |
NMB |
NMB Bank |
NBC |
NBC Bank |
EXIM |
Exim Bank |
STANBIC |
Stanbic Bank |
ABSA |
Absa Bank |
DTB |
Diamond Trust Bank |
KCB |
KCB Bank |