Mamlaka
Payment API
Accept and send payments via mobile money, card, and bank transfer. Integrate seamlessly with M-Pesa, card gateways, and bank APIs across Africa.
REST API
JSON over HTTPS with JWT authentication
Callbacks
Async notifications for payment status
Multi-Channel
M-Pesa, Card, and Bank Transfer support
https://payments.mamlakapsp.com/api/v1Authentication
All authenticated endpoints require an Authorization: Bearer <token> header. First generate a token using Basic Auth via the endpoint.
Key Response Fields
secureIdEncrypted transaction identifier for secure referenceexternalIdYour merchant-provided reference IDtransactionStatusFinal state: COMPLETE, FAILED, or PENDINGtransactionReportDetailed status reportnetAmountAmount received after deductions/feescurrencyTransaction currency code (KES, USD, NGN, etc.)Authentication
Obtain a JWT token required for all subsequent API operations. Use Basic Auth with your merchant credentials.
/api/v1BASICGenerate Token
Obtain a JWT token using your merchant credentials via Basic Authentication. This token is required for all other API calls.
Request
curl -X GET 'https://payments.mamlakapsp.com/api/v1' \
-H 'Authorization: Basic <credentials>'Response
{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"expiresIn": 3600
}Mobile Money
Initiate STK Push payments (C2B) and money transfers (B2C) via mobile money. Set `mobileMoneySP` to "M-PESA" or "airtel" to choose the provider — the same endpoints handle both, so switching a transaction to Airtel is a one-field change. On success, the callback's `reference` field carries the provider receipt (M-Pesa or Airtel). When callback signing is enabled for your account, each callback carries an `X-Mamlaka-Signature: sha256=<hex HMAC-SHA256(rawBody, yourSecret)>` header — verify it against the shared secret Mamlaka issued you, using the exact raw request bytes and a constant-time comparison. Note: Airtel's minimum amount is 10 KES.
/api/v1/mobile/initiatePUBLICSTK Push (C2B)
Initiate an STK push to request payment from a customer's mobile money account. The customer receives a prompt on their phone to authorize the payment. Authenticated by your impalaMerchantId in the body — no bearer token is required. Supports M-Pesa and Airtel — set `mobileMoneySP` to "M-PESA" or "airtel". The result (COMPLETE/FAILED) is delivered via callback once the customer responds.
Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
impalaMerchantId | string | Yes | Your merchant username/ID |
displayName | string | Yes | Business name displayed to the customer on STK prompt |
currency | string | Yes | Currency code (e.g. "KES") |
amount | number | Yes | Amount to charge |
payerPhone | string | Yes | Customer phone number in international format (e.g. "254...") |
mobileMoneySP | string | Yes | Provider: "M-PESA" or "airtel" (case-insensitive). Airtel minimum is 10 KES. |
externalId | string | Yes | Your unique reference for this transaction |
callbackUrl | string | Yes | URL to receive payment status callbacks |
Request
{
"impalaMerchantId": "your_merchant_id",
"displayName": "Your Business Name",
"currency": "KES",
"amount": 10,
"payerPhone": "254712345678",
"mobileMoneySP": "M-Pesa",
"externalId": "order_12345",
"callbackUrl": "https://your-app.com/callback"
}Response
{
"message": "Payment initiation successful",
"secureId": "qdml8553ZeInavKorBHzLA==",
"transactionId": "order_12345"
}/api/v1/mobile/transferBEARERMoney Transfer (B2C)
Transfer money from your merchant account to a customer's mobile money account. Supports M-Pesa and Airtel — set `mobileMoneySP` to "M-PESA" or "airtel". Airtel disbursements typically settle immediately (COMPLETE in the response path); M-Pesa settles via callback.
Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
impalaMerchantId | string | Yes | Your merchant username/ID |
currency | string | Yes | Currency code (e.g. "KES") |
amount | number | Yes | Amount to transfer |
recipientPhone | string | Yes | Recipient phone number in international format (e.g. "254...") |
mobileMoneySP | string | Yes | Provider: "M-PESA" or "airtel" (case-insensitive). Airtel minimum is 10 KES. |
externalId | string | Yes | Your unique reference for this transaction |
callbackUrl | string | Yes | URL to receive transfer status callbacks |
Request
{
"impalaMerchantId": "your_merchant_id",
"currency": "KES",
"amount": 10,
"recipientPhone": "254712345678",
"mobileMoneySP": "M-Pesa",
"externalId": "transfer_12345",
"callbackUrl": "https://your-app.com/callback"
}Response
{
"message": "Payment initiation successful",
"secureId": "qdml8553ZeInavKorBHzLA==",
"transactionId": "a392-45d1-93c1-58f9447915e717138558"
}Card Payments
Process card payments through a secure hosted payment page. Customers are redirected to complete payment.
/api/v1/card/initiateBEARERInitiate Card Payment
Process card payments through a secure payment gateway. Returns a hosted payment link for the customer to complete the transaction.
Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
impalaMerchantId | string | Yes | Your merchant username/ID |
currency | string | Yes | Currency code (e.g. "USD") |
amount | number | Yes | Amount to charge |
mobileMoneySP | string | Yes | Must be "card" |
externalId | string | Yes | Your unique reference for this transaction |
redirectUrl | string | Yes | URL to redirect customer after payment completes |
callbackUrl | string | Yes | URL to receive payment status callbacks |
Request
{
"impalaMerchantId": "your_merchant_id",
"currency": "USD",
"amount": 1,
"mobileMoneySP": "card",
"externalId": "card_order_12345",
"redirectUrl": "https://your-app.com/payment-complete",
"callbackUrl": "https://your-app.com/callback"
}Response
{
"cardLink": "https://payments.mamlakapsp.com/pay?data=...",
"message": "card Payment initiation successful",
"secureId": "OWXJJDUMgPvh9XQhSBy9xg=="
}Bank Transfer
Receive payments via bank transfer. Generate virtual account details for customers to pay into.
/api/v1/bank/payinBEARERBank Transfer (Payin)
Receive payments via bank transfer from customers. Generates temporary bank account details for the customer to transfer funds to.
Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
externalId | string | Yes | Your unique reference for this transaction |
amount | number | Yes | Amount to collect |
currency | string | Yes | Currency code (e.g. "NGN") |
callbackUrl | string | Yes | URL to receive payment status callbacks |
customerName | string | Yes | Name of the paying customer |
customerEmail | string | Yes | Email of the paying customer |
accountName | string | Optional | Label shown to the payer (defaults to "Payment") |
Request
{
"externalId": "bank_order_12345",
"amount": 100,
"currency": "NGN",
"accountName": "Demo account",
"callbackUrl": "https://your-app.com/callback",
"customerName": "John Doe",
"customerEmail": "johndoe@gmail.com"
}Response
{
"secureId": "PSJRU873PGPI",
"externalId": "bank_order_12345",
"status": "pending",
"message": "Bank transfer initiated. Customer should pay to the account below.",
"amount": 100,
"currency": "NGN",
"bankAccountName": "Demo account",
"bankAccountNumber": "5010875892",
"bankName": "vfd",
"bankCode": "566",
"expiryDate": "2026-03-14T01:26:36.263Z"
}Balance
Check your merchant account balances for payins and payouts.
/api/v1/balance/payinsBEARERPayins Balance
Retrieve the current balance of funds received from customers (payins).
Request
curl -X GET 'https://payments.mamlakapsp.com/api/v1/balance/payins' \
-H 'Authorization: Bearer <token>'Response
{
"balance": 15000,
"currency": "KES"
}/api/v1/balance/payoutsBEARERPayouts Balance
Retrieve the current balance available for payouts to customers.
Request
curl -X GET 'https://payments.mamlakapsp.com/api/v1/balance/payouts' \
-H 'Authorization: Bearer <token>'Response
{
"balance": 8500,
"currency": "KES"
}Transactions
Query the status of any transaction using its secure ID or external reference.
/api/v1/transaction?reference={externalId}PUBLICTransaction Status
Look up a transaction's status. Pass the reference as a query parameter: `?reference=<value>`, where the value is your externalId (recommended), or the secureId, merchantRequestID, or checkoutRequestID. Optionally add `&merchant=<impalaMerchantId>`, but it must exactly match the merchant the transaction was booked under — if unsure, omit it. Note: the M-Pesa receipt number is NOT a lookup key. Do not query Safaricom directly (TSQ) — collections settle on Mamlaka's short code, so Safaricom returns "not found"; Mamlaka is the system of record.
Request
curl -X GET 'https://payments.mamlakapsp.com/api/v1/transaction?reference={externalId}'Response
{
"transaction": {
"impalaMerchantId": "your_merchant_id",
"transaction_status": "COMPLETE",
"transaction_report": "collection",
"currency": "KES",
"amount": 10,
"secure_id": "qdml8553ZeInavKorBHzLA==",
"external_id": "order_12345",
"callback_url": "https://your-app.com/callback",
"date_added": 1787138041
}
}Callbacks & Signature
How Mamlaka notifies your callbackUrl of final transaction status, and how to verify the X-Mamlaka-Signature header on every callback.
Callback payload
When a transaction reaches a final state we send an HTTP POST with a JSON body to the callbackUrl you supplied. Respond 200 OK to acknowledge. On success, reference carries the mobile-money receipt number (M-Pesa or Airtel); match on your externalId. The callback shape and signature are identical for M-Pesa and Airtel.
{
"amount": 10,
"currency": "KES",
"externalId": "order_12345",
"secureId": "i1FHTQgFpHP0g-MRacZXUQ==",
"reference": "UHJM32U1U5",
"transactionStatus": "COMPLETE",
"transactionReport": "collection"
}Signature verification — X-Mamlaka-Signature
When callback signing is enabled for your account, every callback carries an X-Mamlaka-Signature header. It is an HMAC-SHA256 of the exact raw request body, keyed with the shared secret Mamlaka issued you, hex-encoded:
X-Mamlaka-Signature: sha256=<hex HMAC-SHA256(rawBody, yourSecret)>Recompute the HMAC over the body as received(before any JSON re-serialization — re-stringifying can change the bytes and break the signature) and compare with a constant-time comparison:
const crypto = require('crypto')
function verifyMamlakaSignature(rawBody, header, secret) {
const expected = 'sha256=' +
crypto.createHmac('sha256', secret).update(rawBody).digest('hex')
const a = Buffer.from(header || '', 'utf8')
const b = Buffer.from(expected, 'utf8')
return a.length === b.length && crypto.timingSafeEqual(a, b)
}
// Express: capture the RAW body so the bytes match what we signed
app.use('/callbacks/mamlaka', express.raw({ type: '*/*' }))
app.post('/callbacks/mamlaka', (req, res) => {
const raw = req.body // Buffer
if (!verifyMamlakaSignature(raw, req.get('X-Mamlaka-Signature'), process.env.MAMLAKA_CALLBACK_SECRET)) {
return res.sendStatus(401)
}
const payload = JSON.parse(raw.toString('utf8'))
// reconcile using payload.externalId + payload.reference ...
res.sendStatus(200)
})