Webhooks Guide
Set up webhooks to receive real-time payment notifications
API Base URL
All webhook API requests should be made to the following base URL:
https://demo-wallet.wearemarz.com/api/v1
Overview
Callbacks are HTTP POST requests sent to your application when payment events occur, such as payment completion or failure. This allows for real-time updates without polling the API.
How to Receive Callbacks
To receive callbacks, simply include a callback_url parameter in any API request that supports callbacks (such as collection requests).
Example Request
POST /api/v1/collections
{
"amount": 10000,
"phone_number": "+256712345678",
"callback_url": "https://your-domain.com/webhook/callback"
}
Important Notes
- Include
callback_urlin your request to receive callbacks - Your callback endpoint must accept POST requests
- Your callback endpoint should return HTTP 200 to acknowledge receipt
- Callbacks are sent asynchronously after transaction processing is complete
Webhook Signing (Optional)
By default, callbacks are sent as plain JSON POST requests — no signature headers. You can optionally enable webhook signing to verify that requests genuinely came from MarzPay.
Your signing secret
MarzPay generates your signing secret in the dashboard when you enable signing.
- Open Business Settings → Webhooks & Security.
- Turn on Sign outgoing webhooks.
- Click Reveal, then Copy.
Signature headers
When signing is enabled, every outgoing callback (direct callback_url and registered webhooks) includes these headers:
X-MarzPay-Timestamp— Unix timestamp (seconds)X-MarzPay-Signature— Format:t={timestamp},v1={hex_signature}
Verification steps
- Read the raw request body exactly as received (the JSON string, before parsing).
- Build a string:
{timestamp}.{raw_body}— use the value fromX-MarzPay-Timestamp. - Hash that string with HMAC-SHA256 and your signing secret. You get a hex string like
a3f8b2c1…. -
Open the
X-MarzPay-Signatureheader. It looks like:t=1712345678,v1=a3f8b2c1d4e5f6…The part afterv1=is MarzPay’s hash. If it exactly matches the hash you computed in step 3, the callback is genuine. If it does not match, reject the request.
In code, use a constant-time compare (e.g. PHP hash_equals()) rather than ==.
Code examples
PHP
$signingSecret = 'your_signing_secret';
$timestamp = $_SERVER['HTTP_X_MARZPAY_TIMESTAMP'] ?? '';
$signatureHeader = $_SERVER['HTTP_X_MARZPAY_SIGNATURE'] ?? '';
$rawBody = file_get_contents('php://input');
$expected = hash_hmac('sha256', $timestamp . '.' . $rawBody, $signingSecret);
preg_match('/v1=([a-f0-9]+)/', $signatureHeader, $matches);
$received = $matches[1] ?? '';
if (!hash_equals($expected, $received)) {
http_response_code(401);
exit('Invalid signature');
}
Node.js
const crypto = require('crypto');
const signingSecret = 'your_signing_secret';
const timestamp = req.headers['x-marzpay-timestamp'] || '';
const signatureHeader = req.headers['x-marzpay-signature'] || '';
const rawBody = req.rawBody; // raw request body string (before JSON.parse)
const expected = crypto
.createHmac('sha256', signingSecret)
.update(`${timestamp}.${rawBody}`)
.digest('hex');
const match = signatureHeader.match(/v1=([a-f0-9]+)/);
const received = match ? match[1] : '';
const valid = received.length === expected.length
&& crypto.timingSafeEqual(Buffer.from(received), Buffer.from(expected));
if (!valid) {
return res.status(401).send('Invalid signature');
}
Python
import hmac
import hashlib
import re
signing_secret = 'your_signing_secret'
timestamp = request.headers.get('X-MarzPay-Timestamp', '')
signature_header = request.headers.get('X-MarzPay-Signature', '')
raw_body = request.get_data(as_text=True)
expected = hmac.new(
signing_secret.encode('utf-8'),
f'{timestamp}.{raw_body}'.encode('utf-8'),
hashlib.sha256,
).hexdigest()
match = re.search(r'v1=([a-f0-9]+)', signature_header)
received = match.group(1) if match else ''
if not hmac.compare_digest(expected, received):
return 'Invalid signature', 401
Java
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
String signingSecret = "your_signing_secret";
String timestamp = request.getHeader("X-MarzPay-Timestamp");
String signatureHeader = request.getHeader("X-MarzPay-Signature");
String rawBody = requestBody; // raw request body string
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(signingSecret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
byte[] hash = mac.doFinal((timestamp + "." + rawBody).getBytes(StandardCharsets.UTF_8));
StringBuilder expected = new StringBuilder();
for (byte b : hash) {
expected.append(String.format("%02x", b));
}
Matcher match = Pattern.compile("v1=([a-f0-9]+)").matcher(signatureHeader);
String received = match.find() ? match.group(1) : "";
if (!expected.toString().equals(received)) {
response.setStatus(401);
return;
}
Callback Flow
Understanding how callbacks work in our system and when you'll receive notifications.
How it works
- You create a collection request with a
callback_url - The customer completes the payment
- MarzPay updates the transaction status (completed, failed, or cancelled)
- Your balance is updated if the payment succeeded
- MarzPay sends a callback to your
callback_url(HTTP POST)
This applies to all payment methods — mobile money, card, and others. The callback payload structure is the same; only the payment details in the payload differ.
When Callbacks Are Sent
Callbacks are sent for ALL final transaction statuses:
- Completed: Payment successful, balance updated
- Failed: Payment failed, no balance change
- Cancelled: Payment cancelled, no balance change
- All internal processing complete
Callbacks are NOT sent for: pending, processing, or any intermediate statuses.
Callback Payload Structure
The structure of the data sent to your callback URL. The event_type and transaction status will vary based on the transaction outcome.
Optional metadata on every callback
If you sent metadata on POST /collect-money or POST /send-money, the same array is included as a top-level metadata field on:
- Direct
callback_urlPOSTs - Dashboard webhooks
- Sandbox callbacks
- Both success and failure final statuses (
*.completed/*.failed)
If you omitted metadata on create, the key is not present on the callback.
"metadata": [
{ "orderId": "ORD-123456789" },
{ "customerId": "customer@email.com", "isPII": true }
]
Do not confuse this with data.metadata on some API create responses (response diagnostics only).
Collection Object Fields
- provider_transaction_id: A unified field containing the transaction ID from the payment provider (MTN, Airtel, or M-Pesa), e.g.
"148769164724". Included on thecollectionobject when the provider returns an ID. Use this field in callbacks — notprovider_reference. - provider_reference: Not sent in collection callbacks. It may appear as
nullon the immediate create-collection API response only; do not model collection webhooks around it. - provider: The payment provider used (
mtn,airtel, ormpesa) - phone_number: The phone number used for the transaction
- amount: The transaction amount with formatted and raw values
- mode: The transaction mode (e.g., mtnuganda, airteluganda, live, sandbox)
- Top-level
metadata(optional): Echo of requestmetadatafrom collect-money — see box above.
Successful Payment Payload (MTN Example)
{
"event_type": "collection.completed",
"transaction": {
"uuid": "transaction-uuid",
"reference": "transaction-reference",
"status": "completed",
"amount": {
"formatted": "10,000.00",
"raw": 10000,
"currency": "UGX"
},
"provider": "mtn",
"phone_number": "+256712345678",
"description": "Payment description",
"created_at": "2025-08-20T15:18:48.000000Z",
"updated_at": "2025-08-20T15:18:48.000000Z"
},
"collection": {
"provider": "mtn",
"phone_number": "+256712345678",
"amount": {
"formatted": "10,000.00",
"raw": 10000,
"currency": "UGX"
},
"mode": "mtnuganda",
"provider_transaction_id": "148769164724"
},
"metadata": [
{ "orderId": "ORD-123456789" },
{ "customerId": "customer@email.com", "isPII": true }
]
}
Successful Payment Payload (Airtel Example)
{
"event_type": "collection.completed",
"transaction": {
"uuid": "transaction-uuid",
"reference": "transaction-reference",
"status": "completed",
"amount": {
"formatted": "10,000.00",
"raw": 10000,
"currency": "UGX"
},
"provider": "airtel",
"phone_number": "+256712345678",
"description": "Payment description",
"created_at": "2025-08-20T15:18:48.000000Z",
"updated_at": "2025-08-20T15:18:48.000000Z"
},
"collection": {
"provider": "airtel",
"phone_number": "+256712345678",
"amount": {
"formatted": "10,000.00",
"raw": 10000,
"currency": "UGX"
},
"mode": "airteluganda",
"provider_transaction_id": "AIRTEL_MONEY_ID"
},
"metadata": [
{ "orderId": "ORD-123456789" },
{ "customerId": "customer@email.com", "isPII": true }
]
}
Successful Payment Payload (M-Pesa / Kenya Example)
{
"event_type": "collection.completed",
"transaction": {
"uuid": "transaction-uuid",
"reference": "transaction-reference",
"status": "completed",
"amount": {
"formatted": "100.00",
"raw": 100,
"currency": "KES"
},
"provider": "mpesa",
"phone_number": "+254710000000",
"description": "Payment description",
"created_at": "2026-08-01T14:30:00.000000Z",
"updated_at": "2026-08-01T14:31:00.000000Z"
},
"collection": {
"provider": "mpesa",
"phone_number": "+254710000000",
"amount": {
"formatted": "100.00",
"raw": 100,
"currency": "KES"
},
"mode": "live",
"provider_transaction_id": "ABC123"
},
"metadata": [
{ "orderId": "ORD-123456789" },
{ "customerId": "customer@email.com", "isPII": true }
]
}
Kenya collect & send overview: Kenya (M-Pesa) guide.
Failed Payment Payload
{
"event_type": "collection.failed",
"transaction": {
"uuid": "transaction-uuid",
"reference": "transaction-reference",
"status": "failed",
"amount": {
"formatted": "10,000.00",
"raw": 10000,
"currency": "UGX"
},
"provider": "mtn",
"phone_number": "+256712345678",
"description": "Payment description",
"created_at": "2025-08-20T15:18:48.000000Z",
"updated_at": "2025-08-20T15:18:48.000000Z"
},
"collection": {
"provider": "mtn",
"phone_number": "+256712345678",
"amount": {
"formatted": "10,000.00",
"raw": 10000,
"currency": "UGX"
},
"mode": "mtnuganda",
"provider_transaction_id": "MTN_FINANCIAL_TRANSACTION_ID"
},
"metadata": [
{ "orderId": "ORD-123456789" },
{ "customerId": "customer@email.com", "isPII": true }
]
}
Failed callbacks also include top-level metadata when you sent it on create.
Card payments callback
Card collections use the same callback format as mobile money. When you provide a callback_url on a card collection, we send an HTTP POST to that URL with a JSON body once the card payment is completed or failed (after the customer returns from the card gateway). The payload structure is identical to the collection examples above; only the provider and some optional fields differ.
Card-specific details
provideris"card payments"(lowercase)phone_numbermay benull(card collections do not require a phone number)provider_transaction_idis the gateway transaction ID from the card provider when availableevent_typeiscollection.completedorcollection.failedas for other collections- Top-level
metadatais included when you sent it on the card collect-money request
Example: Card payment completed
{
"event_type": "collection.completed",
"transaction": {
"uuid": "transaction-uuid",
"reference": "b59d3d6d-5827-41ee-b455-18dd20ef1c8a",
"status": "completed",
"amount": {
"formatted": "5,000.00",
"raw": 5000,
"currency": "UGX"
},
"provider": "card payments",
"phone_number": null,
"description": "Order payment",
"created_at": "2025-08-20T15:18:48.000000Z",
"updated_at": "2025-08-20T15:19:02.000000Z"
},
"collection": {
"provider": "card payments",
"phone_number": null,
"amount": {
"formatted": "5,000.00",
"raw": 5000,
"currency": "UGX"
},
"mode": "card paymentsuganda",
"provider_transaction_id": "PROVIDER_TRANSACTION_ID"
}
}
Example: Card payment failed
{
"event_type": "collection.failed",
"transaction": {
"uuid": "transaction-uuid",
"reference": "b59d3d6d-5827-41ee-b455-18dd20ef1c8a",
"status": "failed",
"amount": { "formatted": "5,000.00", "raw": 5000, "currency": "UGX" },
"provider": "card payments",
"phone_number": null,
"description": "Order payment",
"created_at": "2025-08-20T15:18:48.000000Z",
"updated_at": "2025-08-20T15:19:02.000000Z"
},
"collection": {
"provider": "card payments",
"phone_number": null,
"amount": { "formatted": "5,000.00", "raw": 5000, "currency": "UGX" },
"mode": "card paymentsuganda",
"provider_transaction_id": null
}
}
Your callback endpoint should accept POST with Content-Type: application/json and return HTTP 200. Use event_type and transaction.status to determine success or failure; use transaction.reference to match the collection to your order.
Disbursement Object Fields
- provider_transaction_id: A unified field containing the transaction ID from the payment provider. This single field works for MTN, Airtel, and M-Pesa — it automatically contains the correct provider transaction ID based on which provider processed the disbursement. The field is included in the callback when the provider transaction ID is available.
- provider: The payment provider used (
mtn,airtel, ormpesa) - phone_number: The recipient phone number
- recipient_name: The name of the recipient
- amount: The transaction amount with formatted and raw values
- mode: The transaction mode (e.g., mtnuganda, airteluganda, live, sandbox)
- Top-level
metadata(optional): Echo of requestmetadatafrom send-money — same rules as collections.
Successful Disbursement Payload (Airtel Example)
{
"event_type": "disbursement.completed",
"transaction": {
"uuid": "transaction-uuid",
"reference": "transaction-reference",
"provider_reference": null,
"status": "completed",
"amount": {
"formatted": "1,000.00",
"raw": 1000,
"currency": "UGX"
},
"provider": "airtel",
"phone_number": "+256759983853",
"recipient_name": "Katende Nicholas",
"description": "Send Money to Katende Nicholas",
"created_at": "2025-12-07T05:41:28.000000Z",
"updated_at": "2025-12-07T05:42:05.000000Z"
},
"disbursement": {
"provider": "airtel",
"phone_number": "+256759983853",
"amount": {
"formatted": "1,000.00",
"raw": 1000,
"currency": "UGX"
},
"mode": "airteluganda",
"provider_reference": null,
"recipient_name": "Katende Nicholas",
"provider_transaction_id": "AIRTEL_MONEY_ID"
},
"metadata": [
{ "orderId": "ORD-123456789" },
{ "customerId": "customer@email.com", "isPII": true }
]
}
Top-level metadata is present only when you sent it on POST /send-money.
Successful Disbursement Payload (MTN Example)
{
"event_type": "disbursement.completed",
"transaction": {
"uuid": "transaction-uuid",
"reference": "transaction-reference",
"provider_reference": null,
"status": "completed",
"amount": {
"formatted": "1,000.00",
"raw": 1000,
"currency": "UGX"
},
"provider": "mtn",
"phone_number": "+256712345678",
"recipient_name": "John Doe",
"description": "Send Money to John Doe",
"created_at": "2025-12-07T05:41:28.000000Z",
"updated_at": "2025-12-07T05:42:05.000000Z"
},
"disbursement": {
"provider": "mtn",
"phone_number": "+256712345678",
"amount": {
"formatted": "1,000.00",
"raw": 1000,
"currency": "UGX"
},
"mode": "mtnuganda",
"provider_reference": null,
"recipient_name": "John Doe",
"provider_transaction_id": "MTN_FINANCIAL_TRANSACTION_ID"
},
"metadata": [
{ "orderId": "ORD-123456789" },
{ "customerId": "customer@email.com", "isPII": true }
]
}
Successful Disbursement Payload (M-Pesa / Kenya Example)
{
"event_type": "disbursement.completed",
"transaction": {
"uuid": "transaction-uuid",
"reference": "transaction-reference",
"provider_reference": "123e4567-e89b-12d3-a456-426614174001",
"status": "completed",
"amount": {
"formatted": "100.00",
"raw": 100,
"currency": "KES"
},
"provider": "mpesa",
"phone_number": "+254710000000",
"recipient_name": null,
"description": "Payout to customer",
"created_at": "2026-08-01T14:30:00.000000Z",
"updated_at": "2026-08-01T14:31:00.000000Z"
},
"disbursement": {
"provider": "mpesa",
"phone_number": "+254710000000",
"amount": {
"formatted": "100.00",
"raw": 100,
"currency": "KES"
},
"mode": "live",
"provider_reference": "123e4567-e89b-12d3-a456-426614174001",
"recipient_name": null,
"provider_transaction_id": "ABC123"
},
"metadata": [
{ "orderId": "ORD-123456789" },
{ "customerId": "customer@email.com", "isPII": true }
]
}
Failed Disbursement Payload
{
"event_type": "disbursement.failed",
"transaction": {
"uuid": "transaction-uuid",
"reference": "transaction-reference",
"provider_reference": null,
"status": "failed",
"amount": {
"formatted": "1,000.00",
"raw": 1000,
"currency": "UGX"
},
"provider": "airtel",
"phone_number": "+256759983853",
"recipient_name": "Katende Nicholas",
"description": "Send Money to Katende Nicholas",
"created_at": "2025-12-07T05:41:28.000000Z",
"updated_at": "2025-12-07T05:42:05.000000Z"
},
"disbursement": {
"provider": "airtel",
"phone_number": "+256759983853",
"amount": {
"formatted": "1,000.00",
"raw": 1000,
"currency": "UGX"
},
"mode": "airteluganda",
"provider_reference": null,
"recipient_name": "Katende Nicholas",
"provider_transaction_id": "AIRTEL_MONEY_ID"
},
"metadata": [
{ "orderId": "ORD-123456789" },
{ "customerId": "customer@email.com", "isPII": true }
]
}
Best Practices
Always respond with HTTP 200 to acknowledge receipt of the callback
Implement idempotency to handle duplicate callbacks
Use the transaction reference to track and verify payments
Set up proper error handling and logging for callback processing