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

> Complete reference for webhook payload structure, encryption, and fields

## Overview

All webhook payloads from Modulus Labs are encrypted using JWE (JSON Web Encryption) to protect sensitive transaction data. This page provides a complete reference for the payload structure, encryption details, and all available fields.

<Warning>
  **Always decrypt and verify webhook payloads.** Never trust webhook data without proper JWE decryption and validation. Unencrypted data could be forged or tampered with.
</Warning>

## Webhook Request Structure

When Modulus Labs sends a webhook to your endpoint, the request looks like this:

### HTTP Request

```
POST /your-webhook-endpoint HTTP/1.1
Host: your-domain.com
Content-Type: application/json
activation-code: MERCHANT_CODE_123
```

### Request Body

```json theme={null}
{
  "data": "eyJhbGciOiJBMjU2S1ciLCJlbmMiOiJBMjU2Q0JDLUhTNTEyIn0.9x4mZBJaKc7QPZ..."
}
```

The `data` field contains a JWE-encrypted string that you must decrypt to access the actual transaction details.

## JWE Encryption Specification

Webhook payloads use JSON Web Encryption with the following algorithms:

<CardGroup cols={2}>
  <Card title="Content Encryption" icon="lock">
    **Algorithm:** `A256CBC-HS512`

    AES-256-CBC with HMAC SHA-512 for authenticated encryption
  </Card>

  <Card title="Key Encryption" icon="key">
    **Algorithm:** `A256KW`

    AES-256 Key Wrap for secure key encryption
  </Card>

  <Card title="Serialization Format" icon="file-code">
    **Format:** Compact Serialization

    Standard JWE compact format (five base64url segments separated by dots)
  </Card>

  <Card title="Secret Key" icon="shield">
    **Your Secret:** Modulus Labs Secret Key

    Same key used for QR API authentication
  </Card>
</CardGroup>

### JWE Structure

```
eyJhbGciOiJBMjU2S1ciLCJlbmMiOiJBMjU2Q0JDLUhTNTEyIn0  ← Protected Header
.
9x4mZBJaKc7QPZ-5u8NOBzjK-cQ2puX9eBaWOWgpGJ6SRvb8P8O2xA  ← Encrypted Key
.
Yp8AbjvIrVH46GKQtLV9PA  ← Initialization Vector
.
zlDcH_ES9xg7OdstFYn...  ← Ciphertext (encrypted payload)
.
pL3zxK9bG4mQ7vN8wR2cT  ← Authentication Tag
```

<Tip>
  You don't need to manually parse these segments. JWE libraries handle the decryption process automatically.
</Tip>

## Decrypting the Payload

Use your secret key to decrypt the JWE payload.

### Required Libraries

<Tabs>
  <Tab title="Node.js">
    ```bash theme={null}
    npm install node-jose
    ```
  </Tab>

  <Tab title="Python">
    ```bash theme={null}
    pip install jwcrypto
    ```
  </Tab>

  <Tab title="PHP">
    ```bash theme={null}
    composer require web-token/jwt-framework
    ```
  </Tab>

  <Tab title="Ruby">
    ```bash theme={null}
    gem install json-jwt
    ```
  </Tab>

  <Tab title="Go">
    ```bash theme={null}
    go get github.com/square/go-jose/v3
    ```
  </Tab>
</Tabs>

### Decryption Implementation

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

  async function decryptWebhookPayload(encryptedData, secretKey) {
    try {
      // Create keystore with your secret key
      const keystore = jose.JWK.createKeyStore();
      await keystore.add(secretKey, 'oct');

      // Decrypt JWE
      const result = await jose.JWE.createDecrypt(keystore).decrypt(encryptedData);

      // Parse JSON payload
      const payload = JSON.parse(result.plaintext.toString());

      return payload;
    } catch (error) {
      throw new Error(`Failed to decrypt webhook: ${error.message}`);
    }
  }

  // Usage
  const SECRET_KEY = process.env.MODULUS_SECRET_KEY;
  const webhookData = req.body.data;

  const payload = await decryptWebhookPayload(webhookData, SECRET_KEY);
  console.log('Decrypted payload:', payload);
  ```

  ```python Python theme={null}
  from jwcrypto import jwe, jwk
  import json

  def decrypt_webhook_payload(encrypted_data, secret_key):
      try:
          # Create JWK from secret key
          key = jwk.JWK(kty='oct', k=secret_key)

          # Create JWE object and decrypt
          jwe_token = jwe.JWE()
          jwe_token.deserialize(encrypted_data)
          jwe_token.decrypt(key)

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

          return payload
      except Exception as e:
          raise Exception(f'Failed to decrypt webhook: {str(e)}')

  # Usage
  SECRET_KEY = os.getenv('MODULUS_SECRET_KEY')
  webhook_data = request.json['data']

  payload = decrypt_webhook_payload(webhook_data, SECRET_KEY)
  print(f'Decrypted payload: {payload}')
  ```

  ```php PHP theme={null}
  <?php
  use Jose\Component\Core\JWK;
  use Jose\Component\Encryption\JWEDecrypter;
  use Jose\Component\Encryption\Serializer\CompactSerializer;
  use Jose\Component\Core\AlgorithmManager;
  use Jose\Component\Encryption\Algorithm\KeyEncryption\A256KW;
  use Jose\Component\Encryption\Algorithm\ContentEncryption\A256CBCHS512;

  function decryptWebhookPayload($encryptedData, $secretKey) {
      try {
          // Create JWK from secret key
          $jwk = new JWK([
              'kty' => 'oct',
              'k' => $secretKey
          ]);

          // Setup algorithm managers
          $keyEncryptionAlgorithmManager = new AlgorithmManager([
              new A256KW()
          ]);

          $contentEncryptionAlgorithmManager = new AlgorithmManager([
              new A256CBCHS512()
          ]);

          // Create decrypter
          $jweDecrypter = new JWEDecrypter(
              $keyEncryptionAlgorithmManager,
              $contentEncryptionAlgorithmManager,
              null
          );

          // Deserialize and decrypt
          $serializer = new CompactSerializer();
          $jwe = $serializer->unserialize($encryptedData);
          $jweDecrypter->decryptUsingKey($jwe, $jwk, 0);

          // Parse JSON payload
          $payload = json_decode($jwe->getPayload(), true);

          return $payload;
      } catch (Exception $e) {
          throw new Exception('Failed to decrypt webhook: ' . $e->getMessage());
      }
  }

  // Usage
  $SECRET_KEY = getenv('MODULUS_SECRET_KEY');
  $webhookData = $request['data'];

  $payload = decryptWebhookPayload($webhookData, $SECRET_KEY);
  error_log('Decrypted payload: ' . json_encode($payload));
  ```

  ```ruby Ruby theme={null}
  require 'json/jwt'

  def decrypt_webhook_payload(encrypted_data, secret_key)
    begin
      # Decrypt JWE
      jwe = JSON::JWT.decode(encrypted_data, secret_key)

      return jwe
    rescue => e
      raise "Failed to decrypt webhook: #{e.message}"
    end
  end

  # Usage
  SECRET_KEY = ENV['MODULUS_SECRET_KEY']
  webhook_data = request.body['data']

  payload = decrypt_webhook_payload(webhook_data, SECRET_KEY)
  puts "Decrypted payload: #{payload}"
  ```

  ```go Go theme={null}
  package main

  import (
      "encoding/json"
      "fmt"
      "github.com/square/go-jose/v3"
  )

  func decryptWebhookPayload(encryptedData string, secretKey []byte) (map[string]interface{}, error) {
      // Parse JWE
      object, err := jose.ParseEncrypted(encryptedData)
      if err != nil {
          return nil, fmt.Errorf("failed to parse JWE: %w", err)
      }

      // Decrypt
      plaintext, err := object.Decrypt(secretKey)
      if err != nil {
          return nil, fmt.Errorf("failed to decrypt: %w", err)
      }

      // Parse JSON
      var payload map[string]interface{}
      if err := json.Unmarshal(plaintext, &payload); err != nil {
          return nil, fmt.Errorf("failed to parse JSON: %w", err)
      }

      return payload, nil
  }

  // Usage
  func main() {
      secretKey := []byte(os.Getenv("MODULUS_SECRET_KEY"))
      webhookData := request.Body["data"].(string)

      payload, err := decryptWebhookPayload(webhookData, secretKey)
      if err != nil {
          log.Fatalf("Decryption failed: %v", err)
      }

      fmt.Printf("Decrypted payload: %+v\n", payload)
  }
  ```
</CodeGroup>

## Decrypted Payload Structure

After decryption, the webhook payload is a JSON object with the following structure:

### Complete Field Reference

<ResponseField name="action" type="string" required>
  The webhook action that triggered this notification.

  **Possible values:**

  * `QRPH_SUCCESS` - Payment completed successfully
  * `QRPH_DECLINED` - Payment failed or was declined

  **Example:** `"QRPH_SUCCESS"`
</ResponseField>

<ResponseField name="referenceNumber" type="string" required>
  Unique reference number for this transaction. This matches the `referenceNumber` you provided when creating the Dynamic QR Ph code.

  Use this field to:

  * Match webhook to original QR creation request
  * Prevent duplicate processing (idempotency)
  * Look up order in your database

  **Format:** Alphanumeric string, typically 1-50 characters

  **Example:** `"REF-20240115-ABC123"`
</ResponseField>

<ResponseField name="amount" type="integer" required>
  Transaction amount in the smallest currency unit.

  * For PHP: Amount in centavos (100 = ₱1.00)
  * For USD: Amount in cents (100 = \$1.00)

  **Example:** `100000` represents ₱1,000.00
</ResponseField>

<ResponseField name="currency" type="string" required>
  Three-letter ISO 4217 currency code.

  **Possible values:** `"PHP"`, `"USD"`

  **Example:** `"PHP"`
</ResponseField>

<ResponseField name="transactionDate" type="string" required>
  ISO 8601 timestamp indicating when the transaction was processed by the payment network.

  **Format:** `YYYY-MM-DDTHH:mm:ssZ`

  **Example:** `"2024-01-15T14:30:00Z"`
</ResponseField>

<ResponseField name="status" type="string" required>
  Current transaction status.

  **Possible values:**

  * `SUCCESS` - Payment completed successfully
  * `FAILED` - Payment failed
  * `PENDING` - Payment processing (rare for webhooks)
  * `REQUIRES_ACTION` - Additional action needed (rare for webhooks)

  **Example:** `"SUCCESS"`
</ResponseField>

<ResponseField name="merchantId" type="string" required>
  Your merchant identifier assigned by Modulus Labs.

  **Example:** `"MERCH12345"`
</ResponseField>

<ResponseField name="customerName" type="string">
  Name of the customer who made the payment, as provided by their bank.

  **Availability:** May be `null` if the bank doesn't provide this information.

  **Example:** `"Juan Dela Cruz"`
</ResponseField>

<ResponseField name="bankName" type="string">
  Name of the bank or payment provider the customer used to complete the payment.

  **Availability:** May be `null` if not provided by the payment network.

  **Example:** `"BDO Unibank"`, `"GCash"`, `"Maya"`
</ResponseField>

<ResponseField name="bankReferenceNumber" type="string">
  Bank's internal reference number for this transaction.

  **Availability:** Present only for successful payments, may be `null` for declined payments.

  **Use case:** Reconciliation with bank statements, dispute resolution

  **Example:** `"BANK-REF-987654321"`
</ResponseField>

<ResponseField name="declineReason" type="string">
  Reason the payment was declined (only present when `action` is `QRPH_DECLINED`).

  **Possible values:**

  * `"Insufficient funds"`
  * `"Invalid account"`
  * `"Transaction limit exceeded"`
  * `"Card expired"`
  * `"Declined by bank"`
  * `"Unknown error"`

  **Example:** `"Insufficient funds"`
</ResponseField>

<ResponseField name="metadata" type="object">
  Custom key-value pairs you included when creating the Dynamic QR Ph code. This is returned exactly as you sent it.

  **Use case:** Store order ID, customer ID, or other identifiers to link webhook to your system

  **Example:**

  ```json theme={null}
  {
    "orderId": "ORD-12345",
    "customerId": "CUST-789",
    "productSku": "WIDGET-001"
  }
  ```
</ResponseField>

## Payload Examples

### Successful Payment

```json theme={null}
{
  "action": "QRPH_SUCCESS",
  "referenceNumber": "REF-20240115-ABC123",
  "amount": 100000,
  "currency": "PHP",
  "transactionDate": "2024-01-15T14:30:00Z",
  "status": "SUCCESS",
  "merchantId": "MERCH12345",
  "customerName": "Juan Dela Cruz",
  "bankName": "BDO Unibank",
  "bankReferenceNumber": "BANK-REF-987654321",
  "metadata": {
    "orderId": "ORD-12345",
    "customerId": "CUST-789"
  }
}
```

### Declined Payment

```json theme={null}
{
  "action": "QRPH_DECLINED",
  "referenceNumber": "REF-20240115-XYZ789",
  "amount": 50000,
  "currency": "PHP",
  "transactionDate": "2024-01-15T15:45:00Z",
  "status": "FAILED",
  "merchantId": "MERCH12345",
  "declineReason": "Insufficient funds",
  "metadata": {
    "orderId": "ORD-67890",
    "customerId": "CUST-456"
  }
}
```

### E-wallet Payment (GCash)

```json theme={null}
{
  "action": "QRPH_SUCCESS",
  "referenceNumber": "REF-20240115-GCASH01",
  "amount": 250000,
  "currency": "PHP",
  "transactionDate": "2024-01-15T16:20:00Z",
  "status": "SUCCESS",
  "merchantId": "MERCH12345",
  "customerName": "Maria Santos",
  "bankName": "GCash",
  "bankReferenceNumber": "GCASH-123456789",
  "metadata": {
    "orderId": "ORD-55555",
    "productType": "digital"
  }
}
```

## Field Validation

Validate the decrypted payload before processing:

<CodeGroup>
  ```javascript Node.js theme={null}
  function validateWebhookPayload(payload) {
    const errors = [];

    // Required fields
    if (!payload.action) errors.push('Missing action field');
    if (!payload.referenceNumber) errors.push('Missing referenceNumber field');
    if (typeof payload.amount !== 'number') errors.push('Invalid or missing amount field');
    if (!payload.currency) errors.push('Missing currency field');
    if (!payload.status) errors.push('Missing status field');

    // Valid action values
    const validActions = ['QRPH_SUCCESS', 'QRPH_DECLINED'];
    if (payload.action && !validActions.includes(payload.action)) {
      errors.push(`Invalid action: ${payload.action}`);
    }

    // Valid status values
    const validStatuses = ['SUCCESS', 'FAILED', 'PENDING', 'REQUIRES_ACTION'];
    if (payload.status && !validStatuses.includes(payload.status)) {
      errors.push(`Invalid status: ${payload.status}`);
    }

    // Amount validation
    if (payload.amount <= 0) {
      errors.push('Amount must be positive');
    }

    // Currency validation
    const validCurrencies = ['PHP', 'USD'];
    if (payload.currency && !validCurrencies.includes(payload.currency)) {
      errors.push(`Unsupported currency: ${payload.currency}`);
    }

    if (errors.length > 0) {
      throw new Error(`Payload validation failed: ${errors.join(', ')}`);
    }

    return true;
  }

  // Usage
  try {
    const payload = await decryptWebhookPayload(webhookData, SECRET_KEY);
    validateWebhookPayload(payload);

    // Proceed with processing
    await processWebhook(payload);
  } catch (error) {
    console.error('Invalid webhook:', error.message);
    // Log for investigation but still return 200 OK
  }
  ```

  ```python Python theme={null}
  def validate_webhook_payload(payload):
      errors = []

      # Required fields
      if not payload.get('action'):
          errors.append('Missing action field')
      if not payload.get('referenceNumber'):
          errors.append('Missing referenceNumber field')
      if not isinstance(payload.get('amount'), (int, float)):
          errors.append('Invalid or missing amount field')
      if not payload.get('currency'):
          errors.append('Missing currency field')
      if not payload.get('status'):
          errors.append('Missing status field')

      # Valid action values
      valid_actions = ['QRPH_SUCCESS', 'QRPH_DECLINED']
      if payload.get('action') and payload['action'] not in valid_actions:
          errors.append(f"Invalid action: {payload['action']}")

      # Valid status values
      valid_statuses = ['SUCCESS', 'FAILED', 'PENDING', 'REQUIRES_ACTION']
      if payload.get('status') and payload['status'] not in valid_statuses:
          errors.append(f"Invalid status: {payload['status']}")

      # Amount validation
      if payload.get('amount', 0) <= 0:
          errors.append('Amount must be positive')

      # Currency validation
      valid_currencies = ['PHP', 'USD']
      if payload.get('currency') and payload['currency'] not in valid_currencies:
          errors.append(f"Unsupported currency: {payload['currency']}")

      if errors:
          raise ValueError(f"Payload validation failed: {', '.join(errors)}")

      return True

  # Usage
  try:
      payload = decrypt_webhook_payload(webhook_data, SECRET_KEY)
      validate_webhook_payload(payload)

      # Proceed with processing
      process_webhook(payload)
  except ValueError as e:
      print(f'Invalid webhook: {str(e)}')
      # Log for investigation but still return 200 OK
  ```
</CodeGroup>

## Common Validation Patterns

<AccordionGroup>
  <Accordion title="Verify Reference Number Exists" icon="magnifying-glass">
    Check that the referenceNumber matches a QR code you actually created:

    ```javascript theme={null}
    async function processWebhook(payload) {
      const { referenceNumber } = payload;

      // Look up order in database
      const order = await db.orders.findOne({ referenceNumber });

      if (!order) {
        console.warn(`Unknown reference number: ${referenceNumber}`);
        // Log suspicious webhook for investigation
        await logSuspiciousWebhook(payload);
        return; // Don't process
      }

      // Proceed with processing
      await updateOrderStatus(order, payload);
    }
    ```
  </Accordion>

  <Accordion title="Validate Amount Matches" icon="coins">
    Verify the payment amount matches what you expected:

    ```javascript theme={null}
    async function processWebhook(payload) {
      const order = await db.orders.findOne({ referenceNumber: payload.referenceNumber });

      if (order.amount !== payload.amount) {
        console.error('Amount mismatch', {
          expected: order.amount,
          received: payload.amount,
          referenceNumber: payload.referenceNumber
        });

        // Alert fraud team
        await alertFraudTeam('Amount mismatch detected', { order, payload });

        // Don't fulfill order
        return;
      }

      // Amount matches, proceed
      await fulfillOrder(order);
    }
    ```
  </Accordion>

  <Accordion title="Check Timestamp Freshness" icon="clock">
    Detect replayed or delayed webhooks:

    ```javascript theme={null}
    async function processWebhook(payload) {
      const transactionDate = new Date(payload.transactionDate);
      const now = new Date();
      const ageMinutes = (now - transactionDate) / 1000 / 60;

      if (ageMinutes > 60) {
        console.warn('Old webhook received', {
          referenceNumber: payload.referenceNumber,
          ageMinutes: ageMinutes
        });

        // Still process but log for investigation
        await logDelayedWebhook(payload, ageMinutes);
      }

      await processTransaction(payload);
    }
    ```
  </Accordion>

  <Accordion title="Validate Status Consistency" icon="list-check">
    Ensure action and status fields are consistent:

    ```javascript theme={null}
    function validateStatusConsistency(payload) {
      const { action, status } = payload;

      const validCombinations = {
        'QRPH_SUCCESS': ['SUCCESS'],
        'QRPH_DECLINED': ['FAILED']
      };

      const validStatuses = validCombinations[action];

      if (!validStatuses || !validStatuses.includes(status)) {
        throw new Error(`Inconsistent action (${action}) and status (${status})`);
      }

      return true;
    }
    ```
  </Accordion>
</AccordionGroup>

## Security Considerations

<CardGroup cols={2}>
  <Card title="Always Decrypt" icon="lock">
    Never trust the encrypted payload without decryption. An attacker could send fake webhook requests.
  </Card>

  <Card title="Verify Signature" icon="signature">
    JWE provides authenticated encryption - the decryption process validates data integrity.
  </Card>

  <Card title="Use HTTPS" icon="shield-halved">
    Your webhook endpoint must use HTTPS to prevent man-in-the-middle attacks.
  </Card>

  <Card title="Rate Limiting" icon="gauge">
    Implement rate limiting on your webhook endpoint to prevent abuse.
  </Card>

  <Card title="Log Everything" icon="file-lines">
    Log all webhook receipts, decryption attempts, and processing results.
  </Card>

  <Card title="Handle Errors Gracefully" icon="triangle-exclamation">
    Don't expose sensitive information in error messages.
  </Card>
</CardGroup>

## Troubleshooting Decryption Issues

<AccordionGroup>
  <Accordion title="Decryption Fails with 'Invalid Key' Error" icon="key">
    **Possible causes:**

    * Wrong secret key
    * Secret key for different environment (sandbox vs production)
    * Secret key has extra whitespace or newline characters

    **Solutions:**

    ```javascript theme={null}
    // Trim whitespace from secret key
    const SECRET_KEY = process.env.MODULUS_SECRET_KEY.trim();

    // Verify key format
    console.log('Key length:', SECRET_KEY.length);
    console.log('Key starts with:', SECRET_KEY.substring(0, 3));
    ```
  </Accordion>

  <Accordion title="Payload is Undefined or Null After Decryption" icon="circle-question">
    **Possible causes:**

    * Decryption succeeded but JSON parsing failed
    * Empty payload
    * Incorrect plaintext handling

    **Solutions:**

    ```javascript theme={null}
    const result = await jose.JWE.createDecrypt(keystore).decrypt(encryptedData);

    // Log raw plaintext before parsing
    console.log('Raw plaintext:', result.plaintext.toString());

    // Safely parse JSON
    try {
      const payload = JSON.parse(result.plaintext.toString());
      return payload;
    } catch (error) {
      console.error('JSON parse error:', error);
      console.error('Raw plaintext:', result.plaintext.toString());
      throw error;
    }
    ```
  </Accordion>

  <Accordion title="Algorithm Not Supported Error" icon="gears">
    **Possible causes:**

    * JWE library doesn't support A256CBC-HS512 or A256KW
    * Outdated library version

    **Solutions:**

    ```bash theme={null}
    # Update to latest version
    npm install node-jose@latest

    # Or use alternative library
    npm install jose
    ```

    ```javascript theme={null}
    // Using 'jose' library instead
    const jose = require('jose');

    async function decrypt(encryptedData, secretKey) {
      const secret = new TextEncoder().encode(secretKey);
      const { plaintext } = await jose.compactDecrypt(encryptedData, secret);
      return JSON.parse(new TextDecoder().decode(plaintext));
    }
    ```
  </Accordion>

  <Accordion title="Webhook Data is Corrupted" icon="triangle-exclamation">
    **Possible causes:**

    * Middleware modifying request body
    * Character encoding issues
    * Request body not properly buffered

    **Solutions:**

    ```javascript theme={null}
    // Express: Use raw body parser for webhook endpoint
    app.use('/webhooks/modulus', express.json({
      verify: (req, res, buf) => {
        req.rawBody = buf.toString('utf8');
      }
    }));

    app.post('/webhooks/modulus', (req, res) => {
      console.log('Raw body:', req.rawBody);
      console.log('Parsed body:', req.body);

      // Use parsed body
      const { data } = req.body;
    });
    ```
  </Accordion>
</AccordionGroup>

## Testing with Sample Payloads

Use the Simulate API to generate test webhooks with real encrypted payloads:

```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": "SUCCESS"
  }'
```

This sends a real encrypted webhook to your registered endpoint, allowing you to test decryption end-to-end.

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

## Best Practices

<AccordionGroup>
  <Accordion title="Cache Secret Key" icon="bolt">
    Load your secret key once at startup, not on every webhook:

    ```javascript theme={null}
    // Good: Load once
    const SECRET_KEY = process.env.MODULUS_SECRET_KEY;

    app.post('/webhooks/modulus', async (req, res) => {
      const payload = await decrypt(req.body.data, SECRET_KEY);
    });

    // Bad: Load repeatedly
    app.post('/webhooks/modulus', async (req, res) => {
      const key = process.env.MODULUS_SECRET_KEY; // Don't do this
      const payload = await decrypt(req.body.data, key);
    });
    ```
  </Accordion>

  <Accordion title="Validate Before Processing" icon="circle-check">
    Always validate payload structure before business logic:

    ```javascript theme={null}
    app.post('/webhooks/modulus', async (req, res) => {
      try {
        const payload = await decrypt(req.body.data, SECRET_KEY);

        // Validate first
        validatePayload(payload);

        // Then process
        await processWebhook(payload);

        res.status(200).json({ received: true });
      } catch (error) {
        console.error('Webhook error:', error);
        res.status(200).json({ received: true }); // Still acknowledge
      }
    });
    ```
  </Accordion>

  <Accordion title="Store Raw Encrypted Data" icon="database">
    Save the encrypted payload for audit and debugging:

    ```javascript theme={null}
    await db.webhookLogs.insert({
      referenceNumber: payload.referenceNumber,
      encryptedData: req.body.data, // Store encrypted version
      decryptedPayload: payload,     // Store decrypted version
      receivedAt: new Date(),
      processed: true
    });
    ```
  </Accordion>

  <Accordion title="Handle Partial Data Gracefully" icon="puzzle-piece">
    Some fields like `customerName` or `bankName` may be null:

    ```javascript theme={null}
    const {
      customerName = 'Unknown',
      bankName = 'Unknown',
      declineReason = 'No reason provided'
    } = payload;

    // Use defaults when fields are missing
    console.log(`Payment from ${customerName} via ${bankName}`);
    ```
  </Accordion>
</AccordionGroup>

## Next Steps

<CardGroup cols={2}>
  <Card title="Receiving Webhooks" icon="webhook" href="/docs/webhooks/receiving">
    Complete guide to handling webhook requests
  </Card>

  <Card title="Setup Guide" icon="gear" href="/docs/webhooks/setup">
    Register webhook endpoints
  </Card>

  <Card title="Overview" icon="book" href="/docs/webhooks/overview">
    Webhooks overview and requirements
  </Card>

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