Moneybag

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:

  1. Event Occurs: A payment is completed, subscription renews, or refund is processed
  2. Webhook Triggered: MoneyBag sends HTTP POST to your endpoint with signature
  3. You Verify: Your server verifies the signature for security
  4. You Respond: Return 200 OK to acknowledge receipt
  5. 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 '', 200

2. Register Your Webhook in the Portal

Access: Log in to your Merchant Portal and navigate to Developers → Webhooks to manage your webhook configurations.

  1. Click Create Webhook
  2. Select the event types you want to receive
  3. Enter your endpoint URL (must be HTTPS)
  4. Configure optional settings (retries, timeout, mTLS)
  5. 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:

Create Webhook - Select Events

Payment Events

EventDescription
payment.initiatedPayment session created, awaiting customer action
payment.successPayment completed successfully
payment.failedPayment failed (expired, declined, etc.)
payment.cancelledPayment cancelled by customer or system

Settlement Events

EventDescription
settlement.createdNew settlement batch created
settlement.completedSettlement transferred to merchant
settlement.cancelledSettlement cancelled

Refund Events

EventDescription
refund.initiatedRefund request submitted
refund.successRefund completed
refund.failedRefund failed

Subscription Events

EventDescription
subscription.createdNew subscription created
subscription.trial_startedTrial period began
subscription.trial_endingTrial ending soon (3 days before)
subscription.trial_endedTrial period ended
subscription.activatedSubscription is now active
subscription.payment_succeededRecurring payment successful
subscription.payment_failedRecurring payment failed
subscription.cancelledSubscription cancelled
subscription.expiredSubscription expired
subscription.pausedSubscription paused
subscription.resumedSubscription resumed

Recurring Invoice Events

EventDescription
recurring_invoice.generatedNew recurring invoice created
recurring_invoice.paidRecurring 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:

OptionDescription
Endpoint URLHTTPS URL to receive webhooks (required)
Event TypesEvents to subscribe to - max 20 per webhook
DescriptionOptional label for this webhook
Max RetriesRetry attempts on failure (0-10, default: 5)
TimeoutHTTP timeout in seconds (5-120, default: 30)
mTLSEnable mutual TLS for enhanced security

Important: The secret_key is only shown once when creating the webhook. Store it securely for signature verification.

Save Your Secret Key

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.0

Body:

{
  "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:

  1. Return a 2xx status code (200, 201, 202, 204) to acknowledge receipt
  2. Respond within the configured timeout (default: 30 seconds)
  3. 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

  1. Get the signature (X-Webhook-Signature) and timestamp (X-Webhook-Timestamp) headers
  2. Check timestamp is within 5 minutes of current time (prevents replay attacks)
  3. Construct the signed message: {timestamp}.{raw_body}
  4. Calculate HMAC-SHA256 using your secret key
  5. 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

AttemptDelay After FailureCumulative Time
1Immediate0
21 minute1 minute
35 minutes6 minutes
430 minutes36 minutes
52 hours~2.5 hours
66 hours~8.5 hours

After all retries fail, the delivery is marked as permanently_failed.

Delivery Statuses

StatusDescription
pendingQueued for delivery
in_progressCurrently being sent
successDelivered successfully (2xx response)
failedDelivery failed, will retry
permanently_failedAll retries exhausted

Manual Retry

You can manually retry a failed delivery from the Merchant Portal:

  1. Navigate to Developers → Webhooks
  2. Select the webhook and view its deliveries
  3. Click on the failed delivery
  4. 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 '', 200

2. 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:

  1. Navigate to Developers → Webhooks
  2. Select the webhook you want to test
  3. 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:

  1. Navigate to Developers → Webhooks
  2. Select a webhook to view its deliveries
  3. 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

  1. Check endpoint URL - Verify HTTPS and public accessibility
  2. Check event types - Ensure subscribed to correct events
  3. Check is_active - Webhook must be active
  4. Check firewall - Allow MoneyBag IPs

Signature Verification Failing

  1. Use raw body - Don't parse JSON before verification
  2. Check secret - Ensure using correct whsec_... key
  3. Check timestamp - Ensure server clock is synchronized (NTP)
  4. Check encoding - Use UTF-8 encoding

Deliveries Failing

  1. Check response code - Must return 2xx
  2. Check timeout - Respond within configured timeout
  3. Check logs - Review delivery details for error messages
  4. Check SSL - Ensure valid SSL certificate

Webhook Management

All webhook management is done through the Merchant Portal. Navigate to Developers → Webhooks to:

OperationDescription
Create webhookSet up a new webhook with event types and endpoint
List webhooksView all configured webhooks
View webhook detailsSee configuration, delivery history, and stats
Update webhookModify event types, retries, timeout, or enable/disable
Delete webhookRemove a webhook configuration
Test webhookSend a test payload to your endpoint
Regenerate secretGet a new secret key if compromised
View eventsSee all webhook events triggered
View deliveriesMonitor delivery status and history
Retry deliveryManually retry a failed delivery