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

# Webhook Setup

> Register and configure webhook endpoints to receive QR Ph transaction notifications

## Overview

Setting up webhooks involves two main steps: implementing a webhook endpoint on your server and registering it with Modulus Labs. This guide walks you through the complete setup process.

<Steps>
  <Step title="Implement Webhook Endpoint">
    Create a POST endpoint on your server that can receive and process webhook notifications
  </Step>

  <Step title="Register with Modulus Labs">
    Use the Create Webhook API to register your endpoint URL
  </Step>

  <Step title="Test Your Integration">
    Use the Simulate API to verify your webhook handler works correctly
  </Step>

  <Step title="Go Live">
    Enable your webhook and start receiving real transaction notifications
  </Step>
</Steps>

## Prerequisites

Before registering a webhook, ensure you have:

<CardGroup cols={2}>
  <Card title="Publicly Accessible Endpoint" icon="globe">
    Your webhook URL must be accessible from the internet (not localhost)
  </Card>

  <Card title="HTTPS Support" icon="lock">
    Webhook URLs must use HTTPS for security (HTTP not supported in production)
  </Card>

  <Card title="Secret Key" icon="key">
    Your Modulus Labs secret key for API authentication and JWE decryption
  </Card>

  <Card title="Fast Response Time" icon="gauge-high">
    Endpoint must respond within 10 seconds to avoid timeouts
  </Card>
</CardGroup>

<Warning>
  **Development URLs:** During sandbox testing, you can use services like ngrok, localtunnel, or similar tools to expose localhost endpoints. Never use these in production.
</Warning>

## Step 1: Implement Your Webhook Endpoint

Create a POST endpoint on your server that accepts webhook notifications.

### Basic Webhook Handler Structure

<CodeGroup>
  ```javascript Node.js (Express) theme={null}
  const express = require('express');
  const jose = require('node-jose');
  const app = express();

  app.use(express.json());

  app.post('/webhooks/modulus', async (req, res) => {
    try {
      // Extract encrypted payload
      const { data } = req.body;

      if (!data) {
        return res.status(400).json({ error: 'Missing data field' });
      }

      // Decrypt JWE payload
      const keystore = jose.JWK.createKeyStore();
      await keystore.add(process.env.MODULUS_SECRET_KEY, 'oct');

      const result = await jose.JWE.createDecrypt(keystore).decrypt(data);
      const payload = JSON.parse(result.plaintext.toString());

      // Process webhook based on action
      if (payload.action === 'QRPH_SUCCESS') {
        await handleSuccessfulPayment(payload);
      } else if (payload.action === 'QRPH_DECLINED') {
        await handleDeclinedPayment(payload);
      }

      // Always respond with 200 OK quickly
      res.status(200).json({ received: true });

    } catch (error) {
      console.error('Webhook processing error:', error);
      res.status(500).json({ error: 'Internal server error' });
    }
  });

  async function handleSuccessfulPayment(payload) {
    // Update order status in database
    // Send confirmation email
    // Trigger fulfillment process
    console.log('Payment successful:', payload.referenceNumber);
  }

  async function handleDeclinedPayment(payload) {
    // Update order status to failed
    // Notify customer
    console.log('Payment declined:', payload.referenceNumber);
  }

  app.listen(3000, () => {
    console.log('Webhook server running on port 3000');
  });
  ```

  ```python Python (Flask) theme={null}
  from flask import Flask, request, jsonify
  from jwcrypto import jwe
  from jwcrypto.common import json_encode, json_decode
  import json
  import os

  app = Flask(__name__)

  @app.route('/webhooks/modulus', methods=['POST'])
  def handle_webhook():
      try:
          # Extract encrypted payload
          data = request.json.get('data')

          if not data:
              return jsonify({'error': 'Missing data field'}), 400

          # Decrypt JWE payload
          secret_key = os.getenv('MODULUS_SECRET_KEY')
          jwe_token = jwe.JWE()
          jwe_token.deserialize(data)
          jwe_token.decrypt(secret_key)

          payload = json.loads(jwe_token.payload.decode('utf-8'))

          # Process webhook based on action
          if payload['action'] == 'QRPH_SUCCESS':
              handle_successful_payment(payload)
          elif payload['action'] == 'QRPH_DECLINED':
              handle_declined_payment(payload)

          # Always respond with 200 OK quickly
          return jsonify({'received': True}), 200

      except Exception as e:
          print(f'Webhook processing error: {str(e)}')
          return jsonify({'error': 'Internal server error'}), 500

  def handle_successful_payment(payload):
      # Update order status in database
      # Send confirmation email
      # Trigger fulfillment process
      print(f"Payment successful: {payload['referenceNumber']}")

  def handle_declined_payment(payload):
      # Update order status to failed
      # Notify customer
      print(f"Payment declined: {payload['referenceNumber']}")

  if __name__ == '__main__':
      app.run(port=3000)
  ```

  ```php PHP theme={null}
  <?php
  require 'vendor/autoload.php';

  use Jose\Component\Core\JWK;
  use Jose\Component\Encryption\JWEDecrypter;
  use Jose\Component\Encryption\Serializer\CompactSerializer;

  header('Content-Type: application/json');

  try {
      // Get request body
      $requestBody = file_get_contents('php://input');
      $payload = json_decode($requestBody, true);

      if (empty($payload['data'])) {
          http_response_code(400);
          echo json_encode(['error' => 'Missing data field']);
          exit;
      }

      // Decrypt JWE payload
      $secretKey = getenv('MODULUS_SECRET_KEY');
      $jwk = new JWK([
          'kty' => 'oct',
          'k' => $secretKey
      ]);

      $serializer = new CompactSerializer();
      $jwe = $serializer->unserialize($payload['data']);

      // Decrypt the JWE
      $decrypter = new JWEDecrypter();
      $decrypter->decryptUsingKey($jwe, $jwk, 0);

      $decryptedPayload = json_decode($jwe->getPayload(), true);

      // Process webhook based on action
      if ($decryptedPayload['action'] === 'QRPH_SUCCESS') {
          handleSuccessfulPayment($decryptedPayload);
      } elseif ($decryptedPayload['action'] === 'QRPH_DECLINED') {
          handleDeclinedPayment($decryptedPayload);
      }

      // Always respond with 200 OK quickly
      http_response_code(200);
      echo json_encode(['received' => true]);

  } catch (Exception $e) {
      error_log('Webhook processing error: ' . $e->getMessage());
      http_response_code(500);
      echo json_encode(['error' => 'Internal server error']);
  }

  function handleSuccessfulPayment($payload) {
      // Update order status in database
      // Send confirmation email
      // Trigger fulfillment process
      error_log('Payment successful: ' . $payload['referenceNumber']);
  }

  function handleDeclinedPayment($payload) {
      // Update order status to failed
      // Notify customer
      error_log('Payment declined: ' . $payload['referenceNumber']);
  }
  ```
</CodeGroup>

### Webhook Handler Requirements

<AccordionGroup>
  <Accordion title="Accept POST Requests" icon="arrow-right">
    Your endpoint must accept POST requests with `Content-Type: application/json`.
  </Accordion>

  <Accordion title="Respond Quickly" icon="clock">
    Return `200 OK` within 10 seconds. Process webhook data asynchronously if needed:

    ```javascript theme={null}
    app.post('/webhooks/modulus', async (req, res) => {
      // Immediately acknowledge receipt
      res.status(200).json({ received: true });

      // Process asynchronously
      processWebhookAsync(req.body).catch(console.error);
    });
    ```
  </Accordion>

  <Accordion title="Decrypt JWE Payload" icon="lock">
    All webhook payloads are encrypted with JWE. You must decrypt them using your secret key before processing.

    See [Payload Documentation](/docs/webhooks/payload) for detailed decryption instructions.
  </Accordion>

  <Accordion title="Handle Retries Idempotently" icon="repeat">
    Modulus Labs retries failed webhooks up to 4 times. Use `referenceNumber` to prevent duplicate processing:

    ```javascript theme={null}
    async function processWebhook(payload) {
      // Check if already processed
      const existing = await db.findByReference(payload.referenceNumber);
      if (existing) {
        console.log('Webhook already processed');
        return;
      }

      // Process and mark as handled
      await db.createTransaction(payload);
    }
    ```
  </Accordion>

  <Accordion title="Log All Webhooks" icon="file-lines">
    Log every webhook receipt for debugging and audit purposes:

    ```javascript theme={null}
    console.log('Webhook received:', {
      referenceNumber: payload.referenceNumber,
      action: payload.action,
      timestamp: new Date().toISOString()
    });
    ```
  </Accordion>
</AccordionGroup>

## Step 2: Register Your Webhook

Use the Create Webhook API to register your endpoint with Modulus Labs.

### Create Webhook Request

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST https://webhooks.sbx.moduluslabs.io/v1/webhooks \
    -u sk_your_secret_key: \
    -H "Content-Type: application/json" \
    -d '{
      "webhookUrl": "https://your-domain.com/webhooks/modulus",
      "actions": ["QRPH_SUCCESS", "QRPH_DECLINED"],
      "status": "ENABLED"
    }'
  ```

  ```javascript Node.js theme={null}
  const axios = require('axios');

  const SECRET_KEY = process.env.MODULUS_SECRET_KEY;

  async function createWebhook() {
    try {
      const response = await axios.post(
        'https://webhooks.sbx.moduluslabs.io/v1/webhooks',
        {
          webhookUrl: 'https://your-domain.com/webhooks/modulus',
          actions: ['QRPH_SUCCESS', 'QRPH_DECLINED'],
          status: 'ENABLED'
        },
        {
          auth: {
            username: SECRET_KEY,
            password: ''
          },
          headers: {
            'Content-Type': 'application/json'
          }
        }
      );

      console.log('Webhook created:', response.data);
      console.log('Webhook ID:', response.data.id);

      return response.data;
    } catch (error) {
      console.error('Error creating webhook:', error.response?.data || error.message);
    }
  }

  createWebhook();
  ```

  ```python Python theme={null}
  import requests
  import os

  SECRET_KEY = os.getenv('MODULUS_SECRET_KEY')

  def create_webhook():
      try:
          response = requests.post(
              'https://webhooks.sbx.moduluslabs.io/v1/webhooks',
              json={
                  'webhookUrl': 'https://your-domain.com/webhooks/modulus',
                  'actions': ['QRPH_SUCCESS', 'QRPH_DECLINED'],
                  'status': 'ENABLED'
              },
              auth=(SECRET_KEY, ''),
              headers={
                  'Content-Type': 'application/json'
              }
          )

          response.raise_for_status()

          data = response.json()
          print(f"Webhook created: {data}")
          print(f"Webhook ID: {data['id']}")

          return data

      except requests.exceptions.RequestException as e:
          print(f"Error creating webhook: {str(e)}")
          if hasattr(e, 'response') and e.response is not None:
              print(e.response.json())

  create_webhook()
  ```
</CodeGroup>

### Configuration Options

<ParamField body="webhookUrl" type="string" required>
  The HTTPS URL where Modulus Labs will send webhook notifications. Must be publicly accessible.

  **Example:** `"https://api.yourcompany.com/webhooks/modulus"`
</ParamField>

<ParamField body="actions" type="array" required>
  Array of webhook actions you want to receive. Options: `QRPH_SUCCESS`, `QRPH_DECLINED`, or both.

  **Example:** `["QRPH_SUCCESS", "QRPH_DECLINED"]`

  <Tip>
    Include both actions to handle successful and failed payments. Handling declined payments helps you provide better customer support.
  </Tip>
</ParamField>

<ParamField body="status" type="string" required>
  Webhook status: `ENABLED` or `DISABLED`.

  **Example:** `"ENABLED"`

  Use `DISABLED` if you want to register the webhook but not receive notifications yet (useful during development).
</ParamField>

### Response

```json theme={null}
{
  "id": 123,
  "webhookUrl": "https://your-domain.com/webhooks/modulus",
  "actions": ["QRPH_SUCCESS", "QRPH_DECLINED"],
  "status": "ENABLED",
  "createdAt": "2024-01-15T10:30:00Z",
  "updatedAt": "2024-01-15T10:30:00Z"
}
```

<Note>
  Save the webhook `id` returned in the response. You'll need it to update or delete the webhook later.
</Note>

## Step 3: Test Your Webhook

Use the Simulate API to send test webhooks to your endpoint without creating real transactions.

### Simulate Successful Payment

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST https://webhooks.sbx.moduluslabs.io/v1/webhooks/qrph/simulate \
    -u sk_your_secret_key: \
    -H "Content-Type: application/json" \
    -d '{
      "useCase": "SUCCESS"
    }'
  ```

  ```javascript Node.js theme={null}
  async function testSuccessWebhook() {
    const response = await axios.post(
      'https://webhooks.sbx.moduluslabs.io/v1/webhooks/qrph/simulate',
      { useCase: 'SUCCESS' },
      {
        auth: { username: SECRET_KEY, password: '' }
      }
    );

    console.log('Test webhook sent:', response.data);
  }

  testSuccessWebhook();
  ```

  ```python Python theme={null}
  def test_success_webhook():
      response = requests.post(
          'https://webhooks.sbx.moduluslabs.io/v1/webhooks/qrph/simulate',
          json={'useCase': 'SUCCESS'},
          auth=(SECRET_KEY, '')
      )

      response.raise_for_status()
      print(f"Test webhook sent: {response.json()}")

  test_success_webhook()
  ```
</CodeGroup>

### Simulate Declined Payment

```bash theme={null}
curl -X POST https://webhooks.sbx.moduluslabs.io/v1/webhooks/qrph/simulate \
  -u sk_your_secret_key: \
  -H "Content-Type: application/json" \
  -d '{
    "useCase": "DECLINED"
  }'
```

<Card title="Simulate API Reference" icon="flask" href="/api-reference/webhooks/simulate">
  Complete documentation for testing webhooks in sandbox
</Card>

## Managing Webhooks

### View All Webhooks

Check your registered webhooks:

```bash theme={null}
curl https://webhooks.sbx.moduluslabs.io/v1/webhooks \
  -u sk_your_secret_key:
```

### Update Webhook

Change webhook URL or actions:

```bash theme={null}
curl -X PUT https://webhooks.sbx.moduluslabs.io/v1/webhooks/123 \
  -u sk_your_secret_key: \
  -H "Content-Type: application/json" \
  -d '{
    "webhookUrl": "https://new-domain.com/webhooks/modulus",
    "actions": ["QRPH_SUCCESS"],
    "status": "ENABLED"
  }'
```

### Disable Webhook

Temporarily stop receiving webhooks:

```bash theme={null}
curl -X PUT https://webhooks.sbx.moduluslabs.io/v1/webhooks/123 \
  -u sk_your_secret_key: \
  -H "Content-Type: application/json" \
  -d '{
    "status": "DISABLED"
  }'
```

<Tip>
  Disable webhooks during server maintenance instead of deleting them. Re-enable when ready to resume.
</Tip>

### Delete Webhook

Permanently remove a webhook:

```bash theme={null}
curl -X DELETE https://webhooks.sbx.moduluslabs.io/v1/webhooks/123 \
  -u sk_your_secret_key:
```

## Multi-Merchant Support

If you manage multiple merchants, use the `activation-code` header to identify which merchant account a webhook belongs to.

### Set Activation Code on Webhook Creation

```bash theme={null}
curl -X POST https://webhooks.sbx.moduluslabs.io/v1/webhooks \
  -u sk_your_secret_key: \
  -H "Content-Type: application/json" \
  -H "activation-code: MERCHANT_ABC_123" \
  -d '{
    "webhookUrl": "https://your-domain.com/webhooks/modulus",
    "actions": ["QRPH_SUCCESS", "QRPH_DECLINED"],
    "status": "ENABLED"
  }'
```

### Receive Activation Code in Webhooks

Modulus Labs includes the `activation-code` in webhook requests:

```javascript theme={null}
app.post('/webhooks/modulus', async (req, res) => {
  const activationCode = req.headers['activation-code'];
  const { data } = req.body;

  // Route to appropriate merchant handler
  const merchant = await getMerchantByActivationCode(activationCode);
  await processWebhookForMerchant(merchant, data);

  res.status(200).json({ received: true });
});
```

## Best Practices

<CardGroup cols={2}>
  <Card title="Use HTTPS" icon="lock">
    Always use HTTPS URLs for webhooks in production. HTTP is not secure and may be rejected.
  </Card>

  <Card title="Unique URLs per Environment" icon="code-branch">
    Use different webhook URLs for sandbox and production to avoid mixing test and live data.
  </Card>

  <Card title="Monitor Webhook Health" icon="heart-pulse">
    Set up monitoring alerts if your webhook endpoint becomes unreachable or starts failing.
  </Card>

  <Card title="Version Your Webhook Endpoint" icon="code-branch">
    Include API version in webhook URL path (e.g., `/v1/webhooks/modulus`) to allow gradual migrations.
  </Card>

  <Card title="Implement Replay Protection" icon="shield">
    Use `referenceNumber` and timestamps to detect and ignore duplicate or replayed webhooks.
  </Card>

  <Card title="Graceful Error Handling" icon="triangle-exclamation">
    Return 2xx for successfully received webhooks, even if business logic fails. Log errors for later resolution.
  </Card>
</CardGroup>

## Troubleshooting

<AccordionGroup>
  <Accordion title="Webhook Not Receiving Notifications" icon="question">
    **Possible causes:**

    * Webhook URL is not publicly accessible
    * Firewall blocking incoming requests
    * Webhook status is DISABLED
    * HTTPS certificate issues

    **Solutions:**

    * Test URL accessibility from external network
    * Verify webhook status is ENABLED using GET /v1/webhooks
    * Check server logs for incoming requests
    * Use Simulate API to send test webhook
  </Accordion>

  <Accordion title="Decryption Failures" icon="lock">
    **Possible causes:**

    * Using wrong secret key
    * JWE library not configured correctly
    * Payload corrupted during transmission

    **Solutions:**

    * Verify secret key matches the one used to create QR codes
    * Log raw webhook payload for inspection
    * Test decryption with Simulate API first
    * Check JWE library documentation for your language
  </Accordion>

  <Accordion title="Webhook Timeouts" icon="clock">
    **Possible causes:**

    * Endpoint takes longer than 10 seconds to respond
    * Blocking operations in webhook handler
    * Database queries timing out

    **Solutions:**

    * Return 200 OK immediately, process asynchronously
    * Move heavy operations to background jobs
    * Add request timeout logging
    * Use caching for frequently accessed data
  </Accordion>

  <Accordion title="Duplicate Webhooks" icon="copy">
    **Possible causes:**

    * Webhook retry mechanism (expected behavior)
    * Network issues causing Modulus Labs to resend
    * Endpoint returning non-200 status codes

    **Solutions:**

    * Implement idempotency using referenceNumber
    * Check if webhook was already processed before handling
    * Ensure endpoint returns 200 OK for successful receipts
    * Log webhook IDs to track duplicates
  </Accordion>
</AccordionGroup>

## Security Checklist

<Steps>
  <Step title="Use HTTPS">
    * [ ] Webhook URL uses HTTPS (not HTTP)
    * [ ] SSL certificate is valid and not expired
  </Step>

  <Step title="Verify Webhook Authenticity">
    * [ ] Decrypt JWE payload successfully
    * [ ] Validate payload structure matches expected format
    * [ ] Check referenceNumber exists in your system
  </Step>

  <Step title="Protect Against Replay Attacks">
    * [ ] Track processed referenceNumbers
    * [ ] Reject duplicate webhook deliveries
    * [ ] Validate webhook timestamps are recent
  </Step>

  <Step title="Secure Your Endpoint">
    * [ ] Endpoint doesn't expose sensitive debugging info
    * [ ] Rate limiting configured to prevent abuse
    * [ ] Error messages don't leak system details
  </Step>

  <Step title="Monitor and Alert">
    * [ ] Failed webhook processing triggers alerts
    * [ ] Unusual webhook patterns detected
    * [ ] Decryption failures logged and monitored
  </Step>
</Steps>

## Next Steps

<CardGroup cols={2}>
  <Card title="Receiving Webhooks" icon="webhook" href="/docs/webhooks/receiving">
    Learn how to process webhook notifications
  </Card>

  <Card title="Payload Structure" icon="brackets-curly" href="/docs/webhooks/payload">
    Understand webhook payload format and decryption
  </Card>

  <Card title="Create Webhook API" icon="code" href="/api-reference/webhooks/create">
    Complete API reference for creating webhooks
  </Card>

  <Card title="Simulate API" icon="flask" href="/api-reference/webhooks/simulate">
    Test your webhook integration
  </Card>
</CardGroup>
