Webhooks
Webhooks allow MoneyBag to notify your application in real-time when events occur, such as successful payments, subscription renewals, or refunds. Instead of polling the API, your application receives HTTP POST requests with event data.
Overview
How Webhooks Work
Steps:
- Event Occurs: A payment is completed, subscription renews, or refund is processed
- Webhook Triggered: MoneyBag sends HTTP POST to your endpoint with signature
- You Verify: Your server verifies the signature for security
- You Respond: Return 200 OK to acknowledge receipt
- Retry if Failed: Automatic retries with exponential backoff
Architecture Overview
Key Features
- Real-time notifications for payment, subscription, and refund events
- Multi-event subscriptions - single webhook for multiple event types
- HMAC-SHA256 signatures with timestamp verification
- Automatic retries with exponential backoff (up to 6 attempts)
- Configurable timeouts and retry limits
- Mutual TLS (mTLS) support for enhanced security
- Full delivery history and debugging tools
Quick Start
1. Create a Webhook Endpoint
First, create an endpoint on your server to receive webhooks:
# Example: Flask endpoint
@app.route('/webhooks/moneybag', methods=['POST'])
def handle_webhook():
# Verify signature (see Signature Verification section)
# Process event
return '', 2002. Register Your Webhook in the Portal
Access: Log in to your Merchant Portal and navigate to Developers → Webhooks to manage your webhook configurations.
- Click Create Webhook
- Select the event types you want to receive
- Enter your endpoint URL (must be HTTPS)
- Configure optional settings (retries, timeout, mTLS)
- Click Continue and review your configuration
3. Save Your Secret Key
After creating the webhook, you'll see a dialog with your secret_key (format: whsec_...). Save this immediately - it's only shown once and is required for signature verification.
4. Test Your Webhook
From the webhook details page, click the Test button to send a test payload to your endpoint with event type test.webhook.
Event Types
When creating a webhook in the Merchant Portal, you can select from the following event categories:

Payment Events
| Event | Description |
|---|---|
payment.initiated | Payment session created, awaiting customer action |
payment.success | Payment completed successfully |
payment.failed | Payment failed (expired, declined, etc.) |
payment.cancelled | Payment cancelled by customer or system |
Settlement Events
| Event | Description |
|---|---|
settlement.created | New settlement batch created |
settlement.completed | Settlement transferred to merchant |
settlement.cancelled | Settlement cancelled |
Refund Events
| Event | Description |
|---|---|
refund.initiated | Refund request submitted |
refund.success | Refund completed |
refund.failed | Refund failed |
Subscription Events
| Event | Description |
|---|---|
subscription.created | New subscription created |
subscription.trial_started | Trial period began |
subscription.trial_ending | Trial ending soon (3 days before) |
subscription.trial_ended | Trial period ended |
subscription.activated | Subscription is now active |
subscription.payment_succeeded | Recurring payment successful |
subscription.payment_failed | Recurring payment failed |
subscription.cancelled | Subscription cancelled |
subscription.expired | Subscription expired |
subscription.paused | Subscription paused |
subscription.resumed | Subscription resumed |
Recurring Invoice Events
| Event | Description |
|---|---|
recurring_invoice.generated | New recurring invoice created |
recurring_invoice.paid | Recurring invoice paid |
Webhook Configuration
Multi-Event Subscriptions
A single webhook can subscribe to multiple event types, reducing configuration overhead:
Configuration Options
When creating a webhook in the portal, you can configure:
| Option | Description |
|---|---|
| Endpoint URL | HTTPS URL to receive webhooks (required) |
| Event Types | Events to subscribe to - max 20 per webhook |
| Description | Optional label for this webhook |
| Max Retries | Retry attempts on failure (0-10, default: 5) |
| Timeout | HTTP timeout in seconds (5-120, default: 30) |
| mTLS | Enable mutual TLS for enhanced security |
Important: The secret_key is only shown once when creating the webhook. Store it securely for signature verification.

Managing Webhooks
Webhook management operations are available in the Merchant Portal:
- View all webhooks - See your configured webhooks with their status and event subscriptions
- View webhook details - Check configuration, delivery history, and statistics
- Edit webhook - Update event types, retry settings, timeout, or enable/disable
- Delete webhook - Remove a webhook configuration
- Regenerate secret key - Get a new secret if your current one is compromised
Access: Log in to your Merchant Portal and navigate to Developers → Webhooks to manage your webhook configurations.
Warning: When regenerating a secret key, update your server with the new secret immediately. The old key will stop working.
Receiving Webhooks
Request Format
MoneyBag sends webhooks as HTTP POST requests with the following structure:
Headers:
Content-Type: application/json
X-Webhook-Signature: sha256=<hmac_signature>
X-Webhook-Timestamp: 1701345600
X-Webhook-Event-Type: payment.success
X-Webhook-Event-Id: 550e8400-e29b-41d4-a716-446655440000
User-Agent: MoneyBag-Webhooks/1.0Body:
{
"event_id": "550e8400-e29b-41d4-a716-446655440000",
"event_type": "payment.success",
"occurred_at": "2025-11-30T10:30:00Z",
"merchant_id": 123,
"data": {
"transaction_id": "TXN123456789",
"amount": 1000.00,
"currency": "BDT",
"customer": {
"name": "John Doe",
"email": "john@example.com",
"phone": "+880123456789"
},
"payment_method": "bKash",
"reference": "ORDER-001"
}
}Response Requirements
Your endpoint must:
- Return a
2xxstatus code (200, 201, 202, 204) to acknowledge receipt - Respond within the configured timeout (default: 30 seconds)
- Process the webhook idempotently (handle duplicate deliveries)
Signature Verification
Always verify webhook signatures to ensure requests are from MoneyBag and haven't been tampered with.
Verification Flow
Verification Steps
- Get the signature (
X-Webhook-Signature) and timestamp (X-Webhook-Timestamp) headers - Check timestamp is within 5 minutes of current time (prevents replay attacks)
- Construct the signed message:
{timestamp}.{raw_body} - Calculate HMAC-SHA256 using your secret key
- Compare signatures using constant-time comparison
Python Example
import hmac
import hashlib
import time
def verify_webhook_signature(payload_body, signature_header, timestamp_header, secret_key):
"""
Verify MoneyBag webhook signature.
Args:
payload_body: Raw request body (string)
signature_header: X-Webhook-Signature header value
timestamp_header: X-Webhook-Timestamp header value
secret_key: Your webhook secret key (whsec_...)
Returns:
bool: True if signature is valid
"""
# Check timestamp (reject if > 5 minutes old)
try:
timestamp = int(timestamp_header)
if abs(time.time() - timestamp) > 300:
return False
except (ValueError, TypeError):
return False
# Construct signed message
signed_message = f"{timestamp_header}.{payload_body}"
# Calculate expected signature
expected = hmac.new(
key=secret_key.encode('utf-8'),
msg=signed_message.encode('utf-8'),
digestmod=hashlib.sha256
).hexdigest()
expected_header = f"sha256={expected}"
# Constant-time comparison
return hmac.compare_digest(expected_header, signature_header)Node.js Example
const crypto = require('crypto');
function verifyWebhookSignature(payloadBody, signatureHeader, timestampHeader, secretKey) {
// Check timestamp (reject if > 5 minutes old)
const timestamp = parseInt(timestampHeader, 10);
const currentTime = Math.floor(Date.now() / 1000);
if (Math.abs(currentTime - timestamp) > 300) {
return false;
}
// Construct signed message
const signedMessage = `${timestampHeader}.${payloadBody}`;
// Calculate expected signature
const expected = crypto
.createHmac('sha256', secretKey)
.update(signedMessage)
.digest('hex');
const expectedHeader = `sha256=${expected}`;
// Constant-time comparison
return crypto.timingSafeEqual(
Buffer.from(expectedHeader),
Buffer.from(signatureHeader)
);
}PHP Example
function verifyWebhookSignature($payloadBody, $signatureHeader, $timestampHeader, $secretKey) {
// Check timestamp (reject if > 5 minutes old)
$timestamp = intval($timestampHeader);
$currentTime = time();
if (abs($currentTime - $timestamp) > 300) {
return false;
}
// Construct signed message
$signedMessage = $timestampHeader . '.' . $payloadBody;
// Calculate expected signature
$expected = 'sha256=' . hash_hmac('sha256', $signedMessage, $secretKey);
// Constant-time comparison
return hash_equals($expected, $signatureHeader);
}Retry Policy
MoneyBag automatically retries failed webhook deliveries using exponential backoff:
Retry Flow
Retry Schedule
| Attempt | Delay After Failure | Cumulative Time |
|---|---|---|
| 1 | Immediate | 0 |
| 2 | 1 minute | 1 minute |
| 3 | 5 minutes | 6 minutes |
| 4 | 30 minutes | 36 minutes |
| 5 | 2 hours | ~2.5 hours |
| 6 | 6 hours | ~8.5 hours |
After all retries fail, the delivery is marked as permanently_failed.
Delivery Statuses
| Status | Description |
|---|---|
pending | Queued for delivery |
in_progress | Currently being sent |
success | Delivered successfully (2xx response) |
failed | Delivery failed, will retry |
permanently_failed | All retries exhausted |
Manual Retry
You can manually retry a failed delivery from the Merchant Portal:
- Navigate to Developers → Webhooks
- Select the webhook and view its deliveries
- Click on the failed delivery
- Click the Retry button
Implementation Examples
Flask (Python)
from flask import Flask, request, jsonify
import hmac
import hashlib
import time
import json
import os
app = Flask(__name__)
WEBHOOK_SECRET = os.environ.get('MONEYBAG_WEBHOOK_SECRET')
@app.route('/webhooks/moneybag', methods=['POST'])
def handle_moneybag_webhook():
# Get headers
signature = request.headers.get('X-Webhook-Signature')
timestamp = request.headers.get('X-Webhook-Timestamp')
event_type = request.headers.get('X-Webhook-Event-Type')
event_id = request.headers.get('X-Webhook-Event-Id')
if not all([signature, timestamp, event_type, event_id]):
return jsonify({'error': 'Missing headers'}), 400
# Get raw body BEFORE parsing JSON
payload_body = request.get_data(as_text=True)
# Verify signature
if not verify_webhook_signature(payload_body, signature, timestamp, WEBHOOK_SECRET):
return jsonify({'error': 'Invalid signature'}), 401
# Parse payload
data = json.loads(payload_body)
# Handle event (use idempotency check with event_id)
if event_type == 'payment.success':
handle_payment_success(data)
elif event_type == 'payment.failed':
handle_payment_failed(data)
elif event_type == 'subscription.payment_succeeded':
handle_subscription_renewal(data)
return '', 200
def handle_payment_success(data):
transaction_id = data['data']['transaction_id']
amount = data['data']['amount']
print(f"Payment {transaction_id} succeeded: {amount} BDT")
def handle_payment_failed(data):
transaction_id = data['data']['transaction_id']
print(f"Payment {transaction_id} failed")
def handle_subscription_renewal(data):
subscription_id = data['data']['subscription_uuid']
print(f"Subscription {subscription_id} renewed")Express (Node.js)
const express = require('express');
const crypto = require('crypto');
const app = express();
const WEBHOOK_SECRET = process.env.MONEYBAG_WEBHOOK_SECRET;
// Use raw body for signature verification
app.post('/webhooks/moneybag',
express.raw({ type: 'application/json' }),
(req, res) => {
const signature = req.headers['x-webhook-signature'];
const timestamp = req.headers['x-webhook-timestamp'];
const eventType = req.headers['x-webhook-event-type'];
const eventId = req.headers['x-webhook-event-id'];
if (!signature || !timestamp || !eventType || !eventId) {
return res.status(400).json({ error: 'Missing headers' });
}
const payloadBody = req.body.toString();
if (!verifyWebhookSignature(payloadBody, signature, timestamp, WEBHOOK_SECRET)) {
return res.status(401).json({ error: 'Invalid signature' });
}
const data = JSON.parse(payloadBody);
// Handle event
switch (eventType) {
case 'payment.success':
handlePaymentSuccess(data);
break;
case 'payment.failed':
handlePaymentFailed(data);
break;
case 'subscription.payment_succeeded':
handleSubscriptionRenewal(data);
break;
}
res.status(200).end();
}
);
function handlePaymentSuccess(data) {
const { transaction_id, amount } = data.data;
console.log(`Payment ${transaction_id} succeeded: ${amount} BDT`);
}
function handlePaymentFailed(data) {
const { transaction_id } = data.data;
console.log(`Payment ${transaction_id} failed`);
}
function handleSubscriptionRenewal(data) {
const { subscription_uuid } = data.data;
console.log(`Subscription ${subscription_uuid} renewed`);
}Best Practices
1. Respond Quickly
Return a 200 response immediately after receiving the webhook. Process the event asynchronously if needed:
@app.route('/webhooks/moneybag', methods=['POST'])
def webhook():
# Verify signature...
# Queue for async processing
queue.enqueue(process_webhook, data)
# Return immediately
return '', 2002. Implement Idempotency
Use event_id to prevent duplicate processing:
def handle_webhook(data):
event_id = data['event_id']
# Check if already processed
if is_event_processed(event_id):
return # Skip duplicate
# Process event
process_event(data)
# Mark as processed
mark_event_processed(event_id)3. Store Raw Payloads
Log webhook payloads for debugging:
def handle_webhook(data, raw_body):
save_webhook_log(
event_id=data['event_id'],
event_type=data['event_type'],
raw_payload=raw_body,
received_at=datetime.now()
)4. Handle All Event Types
Always have a default handler for unknown events:
handlers = {
'payment.success': handle_payment_success,
'payment.failed': handle_payment_failed,
# ... other handlers
}
handler = handlers.get(event_type, handle_unknown_event)
handler(data)5. Secure Your Endpoint
- Always verify signatures
- Use HTTPS in production
- Restrict access by IP if possible
- Set appropriate timeouts
Testing
Send Test Webhook
Test your endpoint without triggering real events from the Merchant Portal:
- Navigate to Developers → Webhooks
- Select the webhook you want to test
- Click the Test button
This sends a test payload to your endpoint with event type test.webhook.
View Webhook Events
In the Merchant Portal, navigate to Developers → Webhooks and select a webhook to view its events. You can filter by:
- Event type
- Date range
View Delivery History
Each webhook shows its delivery history with status indicators:
- Success - Delivered successfully
- Failed - Delivery failed, will retry
- Permanently Failed - All retries exhausted
View Delivery Details
Click on any delivery to see full debugging information including:
- Request headers and body sent
- Response status code and body
- Error messages
- Retry schedule
Monitoring & Debugging
Check Delivery Status
Monitor your webhook health in the Merchant Portal:
- Navigate to Developers → Webhooks
- Select a webhook to view its deliveries
- Filter by status (Success, Failed, Permanently Failed)
Retry Failed Deliveries
From the delivery details page, click Retry to manually retry a failed delivery. This queues the delivery for immediate retry.
Delivery Information
Each delivery record includes:
- Status - Current delivery status
- Attempt Count - Number of delivery attempts
- Next Retry - When the next retry will occur (if applicable)
- Request - Headers and body that were sent
- Response - Status code, headers, and body received
- Error - Error message if delivery failed
Troubleshooting
Webhooks Not Receiving Events
- Check endpoint URL - Verify HTTPS and public accessibility
- Check event types - Ensure subscribed to correct events
- Check is_active - Webhook must be active
- Check firewall - Allow MoneyBag IPs
Signature Verification Failing
- Use raw body - Don't parse JSON before verification
- Check secret - Ensure using correct
whsec_...key - Check timestamp - Ensure server clock is synchronized (NTP)
- Check encoding - Use UTF-8 encoding
Deliveries Failing
- Check response code - Must return 2xx
- Check timeout - Respond within configured timeout
- Check logs - Review delivery details for error messages
- Check SSL - Ensure valid SSL certificate
Webhook Management
All webhook management is done through the Merchant Portal. Navigate to Developers → Webhooks to:
| Operation | Description |
|---|---|
| Create webhook | Set up a new webhook with event types and endpoint |
| List webhooks | View all configured webhooks |
| View webhook details | See configuration, delivery history, and stats |
| Update webhook | Modify event types, retries, timeout, or enable/disable |
| Delete webhook | Remove a webhook configuration |
| Test webhook | Send a test payload to your endpoint |
| Regenerate secret | Get a new secret key if compromised |
| View events | See all webhook events triggered |
| View deliveries | Monitor delivery status and history |
| Retry delivery | Manually retry a failed delivery |