Webhooks
MALIPOPAY sends HTTP POST requests to your configured callback URL when payment events occur. Use webhooks to update your system in real time: mark orders as paid, trigger fulfillment, send receipts, etc.
Setup
- Log in to the MALIPOPAY Dashboard.
- Go to Settings > Webhooks.
- Enter your callback URL (must be HTTPS in production).
- Save. MALIPOPAY will send a test ping to verify your endpoint responds with
200 OK.
Callback Payload
When a payment event occurs, MALIPOPAY sends a POST request with this JSON body:
{
"timestamp": "20221002123003",
"reference": "ML00365",
"customerReference": "YOUR REF NUMBER FROM SYSTEM",
"amount": 10000,
"type": "CHARGE",
"merchantAccountId": "Duka Instagram",
"status": "Success",
"customer": {
"firstname": "John",
"lastname": "Deo",
"phoneNumber": "255655128812",
"mno": "Tigo"
},
"payloadSignature": "b875460229adc88cef4bd9904b0b06ba4b2cb4..."
}
Field Reference
| Field | Type | Required | Description |
|---|---|---|---|
timestamp | string | Yes | Format: yyyymmddhhmiss |
reference | string | Yes | Unique payment reference for reconciliation |
customerReference | string | No | Your custom reference (passed during payment initiation) |
amount | number | Yes | Transaction amount in TZS |
type | string | Yes | Transaction type, e.g. CHARGE |
merchantAccountId | string | Yes | Your merchant/project name |
status | string | Yes | Success or Failed |
customer | object | Yes | Customer details (see below) |
payloadSignature | string | Yes | HMAC signature for integrity verification |
Customer object:
| Field | Type | Description |
|---|---|---|
firstname | string | Customer first name |
lastname | string | Customer last name |
phoneNumber | string | Phone number (e.g. 255655128812) |
mno | string | Mobile network operator (Tigo, Vodacom, Airtel, Halotel) |
Signature Verification
Every callback includes a payloadSignature field. Always verify this before processing the payment. The signature is computed as:
SHA256(reference + timestamp + amount + phoneNumber + secret)
Where secret is your project's API secret key (found in your dashboard under Settings > API Keys).
Node.js Example
const crypto = require('crypto');
function verifyWebhook(payload, secret) {
const { reference, timestamp, amount, customer, payloadSignature } = payload;
const computed = crypto
.createHash('sha256')
.update(`${reference}${timestamp}${amount}${customer.phoneNumber}${secret}`)
.digest('hex');
return computed === payloadSignature;
}
// Express handler
app.post('/webhooks/malipopay', express.json(), (req, res) => {
const secret = process.env.MALIPOPAY_SECRET;
if (!verifyWebhook(req.body, secret)) {
console.error('Invalid signature - rejecting callback');
return res.status(401).json({ error: 'Invalid signature' });
}
const { reference, status, amount, customer } = req.body;
console.log(`Payment ${reference}: ${status} - ${amount} TZS from ${customer.phoneNumber}`);
// TODO: update your order/invoice status in your database
res.status(200).json({ received: true });
});
Python Example
import hashlib
from flask import Flask, request, jsonify
app = Flask(__name__)
def verify_webhook(payload, secret):
ref = payload['reference']
ts = payload['timestamp']
amt = str(payload['amount'])
phone = payload['customer']['phoneNumber']
raw = f"{ref}{ts}{amt}{phone}{secret}"
computed = hashlib.sha256(raw.encode()).hexdigest()
return computed == payload['payloadSignature']
@app.route('/webhooks/malipopay', methods=['POST'])
def handle_webhook():
payload = request.get_json()
secret = 'your-api-secret'
if not verify_webhook(payload, secret):
return jsonify(error='Invalid signature'), 401
print(f"Payment {payload['reference']}: {payload['status']}")
# TODO: update your order/invoice status
return jsonify(received=True), 200
Retry Policy
If your endpoint does not respond with a 2xx status code within 30 seconds, MALIPOPAY retries the delivery up to 5 times with exponential backoff:
| Attempt | Delay |
|---|---|
| 1 | 1 minute |
| 2 | 5 minutes |
| 3 | 30 minutes |
| 4 | 2 hours |
| 5 | 12 hours |
After 5 failed attempts, the webhook is marked as failed. You can view failed deliveries in the dashboard and trigger a manual re-send.
Best Practices
- Always verify the signature before processing. Never trust the payload blindly.
- Respond quickly with
200 OKand process asynchronously. Long-running work should be queued. - Be idempotent. You may receive the same callback more than once (retries). Use the
referencefield to deduplicate. - Use HTTPS in production. HTTP endpoints are only accepted in UAT/staging.
Testing
During development, use your UAT API keys and expose your local server via a tunnel:
# Using ngrok
ngrok http 3000
# Copy the https:// URL into your dashboard webhook settings
Send a test payment via the UAT environment and watch the callback arrive at your local endpoint.