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

> Secure your webhook endpoints and verify webhook authenticity

## Overview

Webhook security is critical to ensure that requests to your endpoint are actually from Modulus Labs and haven't been tampered with. All webhook notifications include cryptographic signatures that you must verify before processing.

<Warning>
  **Always verify webhook signatures** before processing webhook data. Failing to verify signatures could allow attackers to send fake transaction notifications to your endpoint.
</Warning>

## How Webhook Security Works

Modulus Labs uses **JWE (JSON Web Encryption)** to secure webhook payloads. Every webhook notification you receive is encrypted and must be decrypted using your encryption key.

<Steps>
  <Step title="We Encrypt the Payload">
    Modulus Labs encrypts the webhook payload using your encryption key with JWE (A256KW algorithm with A256CBC-HS512 encryption)
  </Step>

  <Step title="We Send Encrypted Token">
    The encrypted payload is sent as a `Token` field in the webhook POST request to your endpoint
  </Step>

  <Step title="You Decrypt the Token">
    Your endpoint decrypts the token using your encryption key to access the transaction data
  </Step>

  <Step title="You Process the Event">
    Once decrypted and verified, process the transaction event securely
  </Step>
</Steps>

## Decrypting Webhook Payloads

### Node.js Example

```javascript theme={null}
const jose = require('jose');

app.post('/webhooks/modulus', async (req, res) => {
  try {
    const encryptionKey = process.env.MODULUS_ENCRYPTION_KEY;
    const encryptedToken = req.body.Token;

    // Decrypt the JWE token
    const key = Buffer.from(encryptionKey, 'base64');
    const { plaintext } = await jose.compactDecrypt(encryptedToken, key);

    // Parse the decrypted payload
    const event = JSON.parse(new TextDecoder().decode(plaintext));

    // Verify the event structure
    if (!event.transactionId || !event.status) {
      throw new Error('Invalid webhook payload structure');
    }

    // Process the event
    await handleWebhookEvent(event);

    // Always respond with 200 OK
    res.status(200).json({ received: true });
  } catch (error) {
    console.error('Webhook verification failed:', error);
    res.status(400).json({ error: 'Invalid webhook' });
  }
});
```

### Python Example

```python theme={null}
from jose import jwe
import json

@app.route('/webhooks/modulus', methods=['POST'])
def webhook_handler():
    try:
        encryption_key = os.getenv('MODULUS_ENCRYPTION_KEY')
        encrypted_token = request.json['Token']

        # Decrypt the JWE token
        decrypted = jwe.decrypt(encrypted_token, encryption_key)
        event = json.loads(decrypted)

        # Verify event structure
        if 'transactionId' not in event or 'status' not in event:
            raise ValueError('Invalid webhook payload')

        # Process the event
        handle_webhook_event(event)

        return jsonify({'received': True}), 200
    except Exception as e:
        print(f'Webhook verification failed: {e}')
        return jsonify({'error': 'Invalid webhook'}), 400
```

### C# / .NET Example

```csharp theme={null}
using Jose; // Install-Package jose-jwt

[HttpPost("/webhooks/modulus")]
public async Task<IActionResult> WebhookHandler([FromBody] WebhookRequest request)
{
    try
    {
        var encryptionKey = Environment.GetEnvironmentVariable("MODULUS_ENCRYPTION_KEY");
        var keyBytes = Base64UrlDecode(encryptionKey);

        // Decrypt the JWE token
        var decryptedJson = JWT.Decode(request.Token, keyBytes);
        var webhookEvent = JsonSerializer.Deserialize<WebhookEvent>(decryptedJson);

        // Verify event structure
        if (string.IsNullOrEmpty(webhookEvent.TransactionId) ||
            string.IsNullOrEmpty(webhookEvent.Status))
        {
            return BadRequest(new { error = "Invalid webhook payload" });
        }

        // Process the event
        await HandleWebhookEvent(webhookEvent);

        return Ok(new { received = true });
    }
    catch (Exception ex)
    {
        Console.WriteLine($"Webhook verification failed: {ex.Message}");
        return BadRequest(new { error = "Invalid webhook" });
    }
}
```

## Security Best Practices

<CardGroup cols={2}>
  <Card title="Use HTTPS Only" icon="lock">
    Always use HTTPS endpoints for webhooks. HTTP is not supported in production and is insecure.
  </Card>

  <Card title="Verify All Payloads" icon="shield-check">
    Always decrypt and verify webhook payloads before processing to prevent fake notifications.
  </Card>

  <Card title="Keep Keys Secret" icon="key">
    Never expose your encryption key in client-side code, logs, or version control.
  </Card>

  <Card title="Validate Structure" icon="list-check">
    Always validate that decrypted payloads contain expected fields before processing.
  </Card>

  <Card title="Handle Errors Gracefully" icon="triangle-exclamation">
    Return appropriate HTTP status codes and log verification failures for debugging.
  </Card>

  <Card title="Implement Idempotency" icon="rotate">
    Handle duplicate webhook deliveries by checking transaction IDs you've already processed.
  </Card>
</CardGroup>

## Webhook Payload Structure

After decryption, webhook payloads follow this structure:

### Success Event

```json theme={null}
{
  "transactionId": "0ce32626-08cf-405c-adab-6d384a8871da",
  "merchantReferenceNumber": "ORDER-2025-001",
  "amount": "500.00",
  "currency": "PHP",
  "status": "SUCCESS",
  "timestamp": "2024-01-15T10:30:00Z",
  "qrPhTransactionId": "QRPH123456789",
  "sourceAccount": "09171234567"
}
```

### Declined Event

```json theme={null}
{
  "transactionId": "0ce32626-08cf-405c-adab-6d384a8871da",
  "merchantReferenceNumber": "ORDER-2025-001",
  "amount": "500.00",
  "currency": "PHP",
  "status": "DECLINED",
  "timestamp": "2024-01-15T10:30:00Z",
  "declineReason": "INSUFFICIENT_FUNDS"
}
```

## Implementing Idempotency

Modulus Labs may send the same webhook multiple times (e.g., due to network retries). Implement idempotency to handle duplicates:

```javascript theme={null}
// Track processed transaction IDs
const processedTransactions = new Set();

async function handleWebhookEvent(event) {
  // Check if already processed
  if (processedTransactions.has(event.transactionId)) {
    console.log('Duplicate webhook, already processed:', event.transactionId);
    return; // Skip processing
  }

  // Process the transaction
  await updateOrderStatus(event.merchantReferenceNumber, event.status);

  // Mark as processed
  processedTransactions.add(event.transactionId);

  // Persist to database for durability
  await db.saveProcessedWebhook(event.transactionId);
}
```

## Testing Webhook Security

Use the [Simulate Webhook API](/api-reference/webhooks/simulate) to test your webhook endpoint security in sandbox:

```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 notification to your registered endpoint, allowing you to verify your decryption and validation logic.

## Common Security Issues

<AccordionGroup>
  <Accordion title="Decryption Fails" icon="triangle-exclamation">
    **Symptoms:**

    * Cannot decrypt webhook token
    * "Invalid token" errors

    **Solutions:**

    * Verify you're using the correct encryption key from environment variables
    * Check that encryption key matches the one provided by Modulus Labs
    * Ensure you're using the correct JWE algorithms (A256KW, A256CBC-HS512)
    * Verify no whitespace or encoding issues with the key
  </Accordion>

  <Accordion title="Webhook Endpoint Not Reachable" icon="globe">
    **Symptoms:**

    * Not receiving webhook notifications
    * Webhook delivery failures

    **Solutions:**

    * Verify your endpoint is publicly accessible over the internet
    * Check firewall rules allow incoming HTTPS connections
    * Ensure SSL certificate is valid and not self-signed
    * Test endpoint accessibility from external networks
  </Accordion>

  <Accordion title="Missing Required Fields" icon="list">
    **Symptoms:**

    * Decryption succeeds but payload validation fails
    * Unexpected payload structure

    **Solutions:**

    * Check webhook event type and expected payload structure
    * Verify you're handling both SUCCESS and DECLINED event types
    * Log full decrypted payload to debug unexpected structures
    * Ensure backward compatibility for new fields
  </Accordion>

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

    * Same transaction processed multiple times
    * Duplicate order fulfillment

    **Solutions:**

    * Implement idempotency using transaction ID tracking
    * Use database to persist processed transaction IDs
    * Check transaction ID before processing
    * Use database transactions to prevent race conditions
  </Accordion>
</AccordionGroup>

## IP Whitelisting

For additional security, you can whitelist Modulus Labs webhook IP addresses:

<Note>
  Contact Modulus Labs support to get the list of IP addresses for webhook notifications in your environment (sandbox vs production).
</Note>

Example firewall rule:

```bash theme={null}
# Allow webhooks from Modulus Labs IPs only
iptables -A INPUT -p tcp --dport 443 -s <MODULUS_IP_RANGE> -j ACCEPT
```

## Webhook Retry Logic

Modulus Labs implements automatic retry logic for webhook delivery:

* **Retry Attempts:** Up to 3 additional attempts
* **Retry Interval:** 15 minutes between attempts
* **Success Criteria:** Your endpoint returns `200 OK` within 10 seconds

<Tip>
  Always respond with `200 OK` as quickly as possible. Process webhook events asynchronously if needed to ensure fast response times.
</Tip>

## Next Steps

<CardGroup cols={2}>
  <Card title="Webhook Setup" icon="gear" href="/docs/webhooks/setup">
    Register webhook endpoints
  </Card>

  <Card title="Receiving Webhooks" icon="inbox" href="/docs/webhooks/receiving">
    Handle webhook notifications
  </Card>

  <Card title="Test Webhooks" icon="flask" href="/api-reference/webhooks/simulate">
    Simulate webhook events
  </Card>

  <Card title="Webhook Payload" icon="file-code" href="/docs/webhooks/payload">
    Understand payload structure
  </Card>
</CardGroup>
