> ## Documentation Index
> Fetch the complete documentation index at: https://docs.monei.cc/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhooks

> Receive real-time notifications for events in your Monei account

## 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

```mermaid theme={null}
sequenceDiagram
    participant User
    participant Monei
    participant Your Server
    
    User->>Monei: Initiates transaction
    Monei->>Monei: Processes event
    Monei->>Your Server: POST webhook
    Your Server->>Your Server: Verify signature
    Your Server->>Your Server: Process event
    Your Server->>Monei: 200 OK
    Monei->>Your Server: Retry if failed
```

<Steps>
  <Step title="Event Occurs">
    An event happens in your Monei account (payment, transfer, etc.)
  </Step>

  <Step title="Webhook Sent">
    Monei sends an HTTP POST request to your webhook URL
  </Step>

  <Step title="Signature Verification">
    Your server verifies the webhook signature
  </Step>

  <Step title="Process Event">
    Your server processes the event data
  </Step>

  <Step title="Acknowledge">
    Your server responds with 200 OK
  </Step>

  <Step title="Retry (if needed)">
    Monei retries if your server doesn't respond
  </Step>
</Steps>

***

## Webhook Events

Monei sends webhooks for these events:

<Tabs>
  <Tab title="Naira Wallet">
    **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
  </Tab>

  <Tab title="Offramp">
    **Offramp Events:**

    * `offramp.initiated` - Order created
    * `offramp.deposit_received` - Crypto received
    * `offramp.processing` - Converting to fiat
    * `offramp.fiat_sent` - Fiat sent to bank
    * `offramp.completed` - Transaction complete
    * `offramp.failed` - Transaction failed
  </Tab>

  <Tab title="Transactions">
    **Transaction Events:**

    * `transaction.created` - New transaction
    * `transaction.updated` - Status changed
    * `transaction.completed` - Transaction done
    * `transaction.failed` - Transaction failed
  </Tab>
</Tabs>

***

## Setting Up Webhooks

Configure your webhook endpoint in the Monei dashboard:

1. Go to **Settings** → **Webhooks**
2. Click **Add Webhook**
3. Enter your webhook URL (must be HTTPS)
4. Select events to receive
5. Save and copy your webhook secret

<Warning>
  Your webhook URL must use HTTPS in production. HTTP is only allowed for local testing.
</Warning>

***

## Webhook Payload

Monei sends webhook data in this format:

<CodeGroup>
  ```json Example Payload theme={null}
  {
    "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"
    }
  }
  ```

  ```json Deposit Event theme={null}
  {
    "id": "evt_dep123",
    "type": "deposit.completed",
    "created": 1708000000,
    "data": {
      "reference": "DEP-XYZ789",
      "status": "completed",
      "amount": 50000,
      "currency": "NGN",
      "method": "CARD",
      "accountNumber": "0123456789",
      "bankName": "GTBank"
    }
  }
  ```

  ```json Offramp Event theme={null}
  {
    "id": "evt_off456",
    "type": "offramp.completed",
    "created": 1708000000,
    "data": {
      "reference": "OFFRAMP-ABC123",
      "status": "completed",
      "amount": 100,
      "token": "USDT",
      "network": "base",
      "fiatAmount": 155000,
      "fiatCurrency": "NGN",
      "bankName": "GTBank",
      "accountNumber": "0123456789"
    }
  }
  ```
</CodeGroup>

***

## Webhook Security

### Verify Signatures

**Always verify webhook signatures** to ensure requests are from Monei:

<CodeGroup>
  ```javascript Node.js (Express) theme={null}
  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)
    );
  }
  ```

  ```python Python (Flask) theme={null}
  import hmac
  import hashlib

  @app.route('/webhooks/monei', methods=['POST'])
  def monei_webhook():
      signature = request.headers.get('x-monei-signature')
      webhook_secret = os.getenv('MONEI_WEBHOOK_SECRET')
      
      # Verify signature
      if not verify_signature(request.data, signature, webhook_secret):
          print('Invalid signature - rejected')
          return 'Invalid signature', 401
      
      # Process event
      event = request.json
      print(f'Webhook received: {event["type"]}')
      
      # Handle event
      handle_webhook(event)
      
      # Respond immediately
      return 'OK', 200

  def verify_signature(payload, signature, secret):
      # Compute expected signature
      expected_signature = hmac.new(
          secret.encode(),
          payload,
          hashlib.sha256
      ).hexdigest()
      
      # Compare signatures
      return hmac.compare_digest(signature, expected_signature)
  ```
</CodeGroup>

***

## Handle Webhook Events

Process different event types:

<CodeGroup>
  ```javascript Node.js theme={null}
  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`
    });
  }
  ```

  ```python Python theme={null}
  async def handle_webhook(event):
      print(f'Processing event: {event["type"]}')
      
      event_type = event['type']
      data = event['data']
      
      elif event_type == 'deposit.completed':
          await handle_deposit_completed(data)
      
      elif event_type == 'payout.completed':
          await handle_payout_completed(data)
      
      elif event_type == 'offramp.completed':
          await handle_offramp_completed(data)
      
      else:
          print(f'Unhandled event: {event_type}')

  async def handle_deposit_completed(data):
      print('Deposit completed!')
      print(f"Amount: {data['amount']}")
      
      # Update balance
      await update_user_balance(data['userId'], data['amount'])
      
      # Notify
      await send_notification(
          data['userId'],
          title='Deposit Successful',
          body=f"Wallet credited with ₦{data['amount']}"
      )
  ```
</CodeGroup>

***

## Respond Quickly

**Your webhook endpoint must respond within 10 seconds:**

<CodeGroup>
  ```javascript Node.js theme={null}
  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);
  }
  ```

  ```python Python theme={null}
  @app.route('/webhooks/monei', methods=['POST'])
  def monei_webhook():
      # 1. Verify signature
      if not verify_signature(request.data, request.headers.get('x-monei-signature')):
          return 'Invalid signature', 401
      
      # 2. Acknowledge immediately
      response = make_response('OK', 200)
      
      # 3. Process asynchronously
      event = request.json
      threading.Thread(
          target=process_webhook_async,
          args=(event,)
      ).start()
      
      return response

  def process_webhook_async(event):
      try:
          handle_webhook(event)
      except Exception as error:
          print(f'Webhook processing error: {error}')
  ```
</CodeGroup>

***

## Retry Logic

Monei retries failed webhook deliveries:

| Attempt   | Delay      | Total Time |
| --------- | ---------- | ---------- |
| 1st retry | 1 minute   | 1 min      |
| 2nd retry | 5 minutes  | 6 min      |
| 3rd retry | 30 minutes | 36 min     |
| 4th retry | 2 hours    | 2h 36m     |
| 5th retry | 6 hours    | 8h 36m     |

**Handle idempotency:**

<CodeGroup>
  ```javascript Node.js theme={null}
  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()
    });
  }
  ```

  ```python Python theme={null}
  async def handle_webhook(event):
      # Check if processed
      existing = await db.webhooks.find_one({
          'event_id': event['id']
      })
      
      if existing:
          print(f"Event already processed: {event['id']}")
          return
      
      # Process
      await process_event(event)
      
      # Mark as processed
      await db.webhooks.insert_one({
          'event_id': event['id'],
          'type': event['type'],
          'processed_at': datetime.now()
      })
  ```
</CodeGroup>

***

## Testing Webhooks

### Local Testing with ngrok

<CodeGroup>
  ```bash Terminal theme={null}
  # 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
  ```

  ```javascript server.js theme={null}
  const express = require('express');
  const app = express();

  app.use(express.json());

  app.post('/webhooks/monei', (req, res) => {
    console.log('Webhook received!');
    console.log('Event type:', req.body.type);
    console.log('Data:', req.body.data);
    
    res.status(200).send('OK');
  });

  app.listen(3000, () => {
    console.log('Server running on http://localhost:3000');
  });
  ```
</CodeGroup>

### Test Event Manually

<CodeGroup>
  ```bash cURL theme={null}
  # 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"
      }
    }'
  ```
</CodeGroup>

***

## Best Practices

<CardGroup cols={2}>
  <Card title="Verify Signatures" icon="signature">
    Always verify webhook signatures before processing
  </Card>

  <Card title="Respond Quickly" icon="bolt">
    Acknowledge within 10 seconds, process asynchronously
  </Card>

  <Card title="Handle Duplicates" icon="clone">
    Use event IDs to prevent duplicate processing
  </Card>

  <Card title="Use HTTPS" icon="lock">
    Webhook URLs must use HTTPS in production
  </Card>

  <Card title="Log Events" icon="list">
    Log all webhook events for debugging
  </Card>

  <Card title="Monitor Failures" icon="triangle-exclamation">
    Set up alerts for webhook failures
  </Card>
</CardGroup>

***

## Troubleshooting

<AccordionGroup>
  <Accordion icon="circle-xmark" title="Webhook Not Received">
    **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
  </Accordion>

  <Accordion icon="ban" title="Signature Verification Failed">
    **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`
  </Accordion>

  <Accordion icon="clock" title="Timeout Errors">
    **Problem:** Webhook times out before responding

    **Solution:**

    * Respond with 200 OK immediately
    * Process event asynchronously
    * Don't perform long operations in webhook handler
    * Use background jobs/queues
  </Accordion>

  <Accordion icon="clone" title="Duplicate Events">
    **Problem:** Same event received multiple times

    **Solution:**

    * Store processed event IDs
    * Check if event already processed
    * Make processing idempotent
    * Use database transactions
  </Accordion>
</AccordionGroup>

***

## Next Steps

<CardGroup cols={2}>
  <Card title="Guidelines" icon="shield-halved" href="/security/guidelines">
    Security best practices
  </Card>

  <Card title="Best Practices" icon="shield-check" href="/security/best-practices">
    Additional security tips
  </Card>

  <Card title="Error Handling" icon="triangle-exclamation" href="/core-concepts/error-handling">
    Handle webhook errors
  </Card>

  <Card title="Testing" icon="vial" href="/testing/sandbox">
    Test webhooks in sandbox
  </Card>
</CardGroup>
