Skip to main content

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.
1

Implement Webhook Endpoint

Create a POST endpoint on your server that can receive and process webhook notifications
2

Register with Modulus Labs

Use the Create Webhook API to register your endpoint URL
3

Test Your Integration

Use the Simulate API to verify your webhook handler works correctly
4

Go Live

Enable your webhook and start receiving real transaction notifications

Prerequisites

Before registering a webhook, ensure you have:

Publicly Accessible Endpoint

Your webhook URL must be accessible from the internet (not localhost)

HTTPS Support

Webhook URLs must use HTTPS for security (HTTP not supported in production)

Secret Key

Your Modulus Labs secret key for API authentication and JWE decryption

Fast Response Time

Endpoint must respond within 10 seconds to avoid timeouts
Development URLs: During sandbox testing, you can use services like ngrok, localtunnel, or similar tools to expose localhost endpoints. Never use these in production.

Step 1: Implement Your Webhook Endpoint

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

Basic Webhook Handler Structure

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');
});

Webhook Handler Requirements

Your endpoint must accept POST requests with Content-Type: application/json.
Return 200 OK within 10 seconds. Process webhook data asynchronously if needed:
app.post('/webhooks/modulus', async (req, res) => {
  // Immediately acknowledge receipt
  res.status(200).json({ received: true });

  // Process asynchronously
  processWebhookAsync(req.body).catch(console.error);
});
All webhook payloads are encrypted with JWE. You must decrypt them using your secret key before processing.See Payload Documentation for detailed decryption instructions.
Modulus Labs retries failed webhooks up to 4 times. Use referenceNumber to prevent duplicate processing:
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);
}
Log every webhook receipt for debugging and audit purposes:
console.log('Webhook received:', {
  referenceNumber: payload.referenceNumber,
  action: payload.action,
  timestamp: new Date().toISOString()
});

Step 2: Register Your Webhook

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

Create Webhook Request

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"
  }'

Configuration Options

webhookUrl
string
required
The HTTPS URL where Modulus Labs will send webhook notifications. Must be publicly accessible.Example: "https://api.yourcompany.com/webhooks/modulus"
actions
array
required
Array of webhook actions you want to receive. Options: QRPH_SUCCESS, QRPH_DECLINED, or both.Example: ["QRPH_SUCCESS", "QRPH_DECLINED"]
Include both actions to handle successful and failed payments. Handling declined payments helps you provide better customer support.
status
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).

Response

{
  "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"
}
Save the webhook id returned in the response. You’ll need it to update or delete the webhook later.

Step 3: Test Your Webhook

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

Simulate Successful Payment

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"
  }'

Simulate Declined Payment

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"
  }'

Simulate API Reference

Complete documentation for testing webhooks in sandbox

Managing Webhooks

View All Webhooks

Check your registered webhooks:
curl https://webhooks.sbx.moduluslabs.io/v1/webhooks \
  -u sk_your_secret_key:

Update Webhook

Change webhook URL or actions:
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:
curl -X PUT https://webhooks.sbx.moduluslabs.io/v1/webhooks/123 \
  -u sk_your_secret_key: \
  -H "Content-Type: application/json" \
  -d '{
    "status": "DISABLED"
  }'
Disable webhooks during server maintenance instead of deleting them. Re-enable when ready to resume.

Delete Webhook

Permanently remove a webhook:
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

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

Use HTTPS

Always use HTTPS URLs for webhooks in production. HTTP is not secure and may be rejected.

Unique URLs per Environment

Use different webhook URLs for sandbox and production to avoid mixing test and live data.

Monitor Webhook Health

Set up monitoring alerts if your webhook endpoint becomes unreachable or starts failing.

Version Your Webhook Endpoint

Include API version in webhook URL path (e.g., /v1/webhooks/modulus) to allow gradual migrations.

Implement Replay Protection

Use referenceNumber and timestamps to detect and ignore duplicate or replayed webhooks.

Graceful Error Handling

Return 2xx for successfully received webhooks, even if business logic fails. Log errors for later resolution.

Troubleshooting

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

Security Checklist

1

Use HTTPS

  • Webhook URL uses HTTPS (not HTTP)
  • SSL certificate is valid and not expired
2

Verify Webhook Authenticity

  • Decrypt JWE payload successfully
  • Validate payload structure matches expected format
  • Check referenceNumber exists in your system
3

Protect Against Replay Attacks

  • Track processed referenceNumbers
  • Reject duplicate webhook deliveries
  • Validate webhook timestamps are recent
4

Secure Your Endpoint

  • Endpoint doesn’t expose sensitive debugging info
  • Rate limiting configured to prevent abuse
  • Error messages don’t leak system details
5

Monitor and Alert

  • Failed webhook processing triggers alerts
  • Unusual webhook patterns detected
  • Decryption failures logged and monitored

Next Steps