Skip to main content

Overview

Webhooks allow you to receive real-time HTTP notifications when events occur in your Monei account. Instead of polling the API, Monei pushes updates directly to your server. What you’ll learn:
  • Setting up webhooks
  • Webhook events
  • Verifying webhook signatures
  • Handling webhook deliveries
  • Retry logic
  • Best practices

How Webhooks Work

1

Event Occurs

An event happens in your Monei account (payment, transfer, etc.)
2

Webhook Sent

Monei sends an HTTP POST request to your webhook URL
3

Signature Verification

Your server verifies the webhook signature
4

Process Event

Your server processes the event data
5

Acknowledge

Your server responds with 200 OK
6

Retry (if needed)

Monei retries if your server doesn’t respond

Webhook Events

Monei sends webhooks for these events:
Deposit Events:
  • deposit.initiated - Deposit started
  • deposit.pending - Awaiting confirmation
  • deposit.completed - Deposit successful
  • deposit.failed - Deposit failed
Payout Events:
  • payout.initiated - Payout started
  • payout.processing - Being processed
  • payout.completed - Payout successful
  • payout.failed - Payout failed

Setting Up Webhooks

Configure your webhook endpoint in the Monei dashboard:
  1. Go to SettingsWebhooks
  2. Click Add Webhook
  3. Enter your webhook URL (must be HTTPS)
  4. Select events to receive
  5. Save and copy your webhook secret
Your webhook URL must use HTTPS in production. HTTP is only allowed for local testing.

Webhook Payload

Monei sends webhook data in this format:
{
  "id": "evt_abc123xyz",
  "type": "bill.payment.successful",
  "created": 1708000000,
  "data": {
    "reference": "BILL-ABC123XYZ",
    "status": "successful",
    "amount": 1000,
    "currency": "NGN",
    "billerName": "MTN Nigeria",
    "customerName": "JOHN DOE",
    "customerId": "08012345678"
  }
}

Webhook Security

Verify Signatures

Always verify webhook signatures to ensure requests are from Monei:
const crypto = require('crypto');

app.post('/webhooks/monei', (req, res) => {
  const signature = req.headers['x-monei-signature'];
  const webhookSecret = process.env.MONEI_WEBHOOK_SECRET;
  
  // Verify signature
  if (!verifySignature(req.body, signature, webhookSecret)) {
    console.log('Invalid signature - rejected');
    return res.status(401).send('Invalid signature');
  }
  
  // Process event
  const event = req.body;
  console.log('Webhook received:', event.type);
  
  // Handle event
  handleWebhook(event);
  
  // Respond immediately
  res.status(200).send('OK');
});

function verifySignature(payload, signature, secret) {
  // Compute expected signature
  const expectedSignature = crypto
    .createHmac('sha256', secret)
    .update(JSON.stringify(payload))
    .digest('hex');
  
  // Compare signatures
  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expectedSignature)
  );
}

Handle Webhook Events

Process different event types:
async function handleWebhook(event) {
  console.log(`Processing event: ${event.type}`);
  
  switch (event.type) {
    case 'deposit.completed':
      await handleDepositCompleted(event.data);
      break;
      
    case 'payout.completed':
      await handlePayoutCompleted(event.data);
      break;
      
    case 'offramp.completed':
      await handleOfframpCompleted(event.data);
      break;
      
    case 'offramp.failed':
      await handleOfframpFailed(event.data);
      break;
      
    default:
      console.log(`Unhandled event type: ${event.type}`);
  }
}


async function handleDepositCompleted(data) {
  console.log('Deposit completed!');
  console.log('Amount:', data.amount, data.currency);
  
  // Update balance
  await updateUserBalance(data.userId, data.amount);
  
  // Notify user
  await sendNotification(data.userId, {
    title: 'Deposit Successful',
    body: `Your wallet has been credited with ₦${data.amount}`
  });
}

async function handleOfframpCompleted(data) {
  console.log('Offramp completed!');
  console.log('Crypto:', data.amount, data.token);
  console.log('Fiat:', data.fiatAmount, data.fiatCurrency);
  console.log('Bank:', data.bankName);
  
  // Update database
  await db.offramp.update({
    reference: data.reference,
    status: 'completed'
  });
  
  // Notify user
  await sendNotification(data.userId, {
    title: 'Offramp Complete',
    body: `₦${data.fiatAmount} sent to your ${data.bankName} account`
  });
}

Respond Quickly

Your webhook endpoint must respond within 10 seconds:
app.post('/webhooks/monei', async (req, res) => {
  // 1. Verify signature
  if (!verifySignature(req.body, req.headers['x-monei-signature'])) {
    return res.status(401).send('Invalid signature');
  }
  
  // 2. Acknowledge immediately
  res.status(200).send('OK');
  
  // 3. Process asynchronously (don't await)
  processWebhookAsync(req.body).catch(error => {
    console.error('Webhook processing error:', error);
  });
});

async function processWebhookAsync(event) {
  // Time-consuming operations here
  await handleWebhook(event);
}

Retry Logic

Monei retries failed webhook deliveries:
AttemptDelayTotal Time
1st retry1 minute1 min
2nd retry5 minutes6 min
3rd retry30 minutes36 min
4th retry2 hours2h 36m
5th retry6 hours8h 36m
Handle idempotency:
async function handleWebhook(event) {
  // Check if already processed
  const existing = await db.webhooks.findOne({
    eventId: event.id
  });
  
  if (existing) {
    console.log('Event already processed:', event.id);
    return; // Skip duplicate
  }
  
  // Process event
  await processEvent(event);
  
  // Mark as processed
  await db.webhooks.create({
    eventId: event.id,
    type: event.type,
    processedAt: new Date()
  });
}

Testing Webhooks

Local Testing with ngrok

# Install ngrok
brew install ngrok  # macOS
# or
npm install -g ngrok

# Start your server
node server.js  # Running on port 3000

# In another terminal, start ngrok
ngrok http 3000

# Copy the HTTPS URL (e.g., https://abc123.ngrok.io)
# Use this as your webhook URL in Monei dashboard

Test Event Manually

# Simulate webhook event
curl -X POST http://localhost:3000/webhooks/monei \
  -H "Content-Type: application/json" \
  -H "x-monei-signature: test_signature" \
  -d '{
    "id": "evt_test123",
    "type": "bill.payment.successful",
    "created": 1708000000,
    "data": {
      "reference": "BILL-TEST123",
      "status": "successful",
      "amount": 1000,
      "currency": "NGN"
    }
  }'

Best Practices

Verify Signatures

Always verify webhook signatures before processing

Respond Quickly

Acknowledge within 10 seconds, process asynchronously

Handle Duplicates

Use event IDs to prevent duplicate processing

Use HTTPS

Webhook URLs must use HTTPS in production

Log Events

Log all webhook events for debugging

Monitor Failures

Set up alerts for webhook failures

Troubleshooting

Possible causes:
  • Incorrect webhook URL
  • URL not accessible from internet
  • Firewall blocking requests
  • Server down
Solutions:
  • Verify URL is correct and HTTPS
  • Test with ngrok for local development
  • Check firewall rules
  • Verify server is running
  • Check webhook logs in Monei dashboard
Possible causes:
  • Wrong webhook secret
  • Modified request body
  • Incorrect signature algorithm
Solutions:
  • Verify webhook secret from dashboard
  • Don’t modify request body before verification
  • Use correct HMAC SHA-256 algorithm
  • Check signature header name: x-monei-signature
Problem: Webhook times out before respondingSolution:
  • Respond with 200 OK immediately
  • Process event asynchronously
  • Don’t perform long operations in webhook handler
  • Use background jobs/queues
Problem: Same event received multiple timesSolution:
  • Store processed event IDs
  • Check if event already processed
  • Make processing idempotent
  • Use database transactions

Next Steps

Guidelines

Security best practices

Best Practices

Additional security tips

Error Handling

Handle webhook errors

Testing

Test webhooks in sandbox