Skip to main content

Common Issues and Solutions

Webhook Not Being Received

Verify your webhook is properly configured and enabled:
curl -X GET "https://{subdomain}.nudj.cx/api/v2/admin/webhooks/configs" \
  -H "x-api-token: YOUR_ADMIN_API_TOKEN"
Common issues:
  • isEnabled: false - Webhook is disabled
  • Empty events array - No events configured
  • Invalid URL format - Must be valid HTTPS URL
  • Missing or incorrect headers
Test if your webhook endpoint is reachable:
# Test basic connectivity
curl -X POST "https://your-app.com/webhooks/nudj" \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer your-webhook-secret" \
  -d '{"test": "connectivity"}'
Requirements:
  • Must use HTTPS (not HTTP)
  • Must respond within 10 seconds
  • Must return HTTP 2xx for success
  • No redirects (301/302) allowed
Ensure the events you’re expecting are actually occurring:
// Log all webhook configurations to verify event subscriptions
const configs = await fetch(`https://${subdomain}.nudj.cx/api/v2/admin/webhooks/configs`, {
  headers: { 'x-api-token': 'YOUR_TOKEN' }
}).then(r => r.json());

console.log('Subscribed events:', configs[0]?.events);
Verify:
  • Events are occurring in your community
  • Event names match exactly (case-sensitive)
  • User actions are completing successfully

Authentication Failures

Nudj sends custom headers that your endpoint must verify:
// Correct way to verify webhook authenticity
function verifyWebhookSignature(req) {
  const expectedAuth = `Bearer ${process.env.WEBHOOK_SECRET}`;
  const receivedAuth = req.get('Authorization');
  const webhookSource = req.get('X-Webhook-Source');
  
  if (receivedAuth !== expectedAuth) {
    console.error('Auth header mismatch:', { expected: expectedAuth, received: receivedAuth });
    return false;
  }
  
  if (webhookSource !== 'nudj-platform') {
    console.error('Invalid webhook source:', webhookSource);
    return false;
  }
  
  return true;
}
Common mistakes:
  • Case sensitivity in header names
  • Missing Bearer prefix
  • Wrong environment variable
  • Headers not configured in webhook settings
Ensure your secrets are properly configured:
// Debug environment variables
console.log('Environment check:', {
  hasWebhookSecret: !!process.env.WEBHOOK_SECRET,
  secretLength: process.env.WEBHOOK_SECRET?.length,
  hasApiToken: !!process.env.NUDJ_ADMIN_API_TOKEN
});
Best practices:
  • Use different secrets for development/production
  • Never commit secrets to version control
  • Use a secure secret generator
  • Rotate secrets regularly

Processing Errors

Webhooks must respond within 10 seconds:
// Implement async processing for heavy workloads
app.post('/webhooks/nudj', async (req, res) => {
  try {
    // Respond immediately
    res.status(200).send('OK');
    
    // Process asynchronously
    setImmediate(async () => {
      try {
        await processWebhookEvent(req.body);
      } catch (error) {
        console.error('Async webhook processing error:', error);
      }
    });
  } catch (error) {
    console.error('Webhook error:', error);
    res.status(500).send('Error');
  }
});
Optimize webhook processing for high throughput:
// Use connection pooling
const pool = new Pool({
  connectionString: process.env.DATABASE_URL,
  max: 20
});

// Implement rate limiting
const rateLimit = require('express-rate-limit');
const webhookLimiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 1000, // limit each IP to 1000 requests per windowMs
  message: 'Too many webhook requests'
});

app.use('/webhooks', webhookLimiter);
Handle malformed webhook payloads gracefully:
app.use('/webhooks', express.json({
  limit: '10mb',
  verify: (req, res, buf, encoding) => {
    try {
      JSON.parse(buf);
    } catch (error) {
      console.error('Invalid JSON in webhook:', error);
      throw new Error('Invalid JSON payload');
    }
  }
}));

Debugging Techniques

Enable Detailed Logging

const winston = require('winston');

const logger = winston.createLogger({
  level: process.env.LOG_LEVEL || 'info',
  format: winston.format.combine(
    winston.format.timestamp(),
    winston.format.errors({ stack: true }),
    winston.format.json()
  ),
  transports: [
    new winston.transports.Console(),
    new winston.transports.File({ filename: 'webhook-debug.log' })
  ]
});

app.post('/webhooks/nudj', async (req, res) => {
  const webhookId = req.body?.id || 'unknown';
  
  logger.info('Webhook received', {
    webhookId,
    headers: req.headers,
    body: req.body,
    timestamp: new Date().toISOString()
  });
  
  try {
    // Verify signature
    if (!verifyWebhookSignature(req)) {
      logger.warn('Webhook signature verification failed', {
        webhookId,
        receivedAuth: req.get('Authorization'),
        receivedSource: req.get('X-Webhook-Source')
      });
      return res.status(401).send('Unauthorized');
    }
    
    logger.info('Webhook signature verified', { webhookId });
    
    // Process event
    await processWebhookEvent(req.body);
    
    logger.info('Webhook processed successfully', { webhookId });
    res.status(200).send('OK');
    
  } catch (error) {
    logger.error('Webhook processing failed', {
      webhookId,
      error: error.message,
      stack: error.stack
    });
    res.status(500).send('Internal Server Error');
  }
});

Request Inspection Tools

function requestInspector(req, res, next) {
  const originalSend = res.send;
  const startTime = Date.now();
  
  // Log incoming request
  console.log('=== INCOMING WEBHOOK ===');
  console.log('Method:', req.method);
  console.log('URL:', req.url);
  console.log('Headers:', JSON.stringify(req.headers, null, 2));
  console.log('Body:', JSON.stringify(req.body, null, 2));
  console.log('Timestamp:', new Date().toISOString());
  
  // Override res.send to log response
  res.send = function(data) {
    const duration = Date.now() - startTime;
    console.log('=== OUTGOING RESPONSE ===');
    console.log('Status:', res.statusCode);
    console.log('Duration:', duration + 'ms');
    console.log('Response:', data);
    console.log('========================');
    
    return originalSend.call(this, data);
  };
  
  next();
}

// Use only in development
if (process.env.NODE_ENV === 'development') {
  app.use('/webhooks', requestInspector);
}

Webhook Testing Tools

// webhook-tester.js
const express = require('express');
const app = express();

app.use(express.json());

app.post('/webhooks/nudj', (req, res) => {
  console.log('=== WEBHOOK TEST ===');
  console.log('Headers:', req.headers);
  console.log('Body:', JSON.stringify(req.body, null, 2));
  console.log('==================');
  
  res.status(200).json({
    message: 'Webhook received successfully',
    timestamp: new Date().toISOString(),
    webhookId: req.body?.id
  });
});

const port = process.env.PORT || 3000;
app.listen(port, () => {
  console.log(`Webhook test server running on port ${port}`);
  console.log(`Use ngrok to expose: ngrok http ${port}`);
});

Error Monitoring and Alerting

Webhook Health Checks

class WebhookHealthMonitor {
  constructor() {
    this.stats = {
      totalReceived: 0,
      totalProcessed: 0,
      totalFailed: 0,
      lastProcessed: null,
      avgProcessingTime: 0
    };
    
    this.processingTimes = [];
    this.maxSamples = 100;
  }
  
  recordWebhook(processingTime, success) {
    this.stats.totalReceived++;
    
    if (success) {
      this.stats.totalProcessed++;
      this.stats.lastProcessed = new Date();
      
      // Track processing time
      this.processingTimes.push(processingTime);
      if (this.processingTimes.length > this.maxSamples) {
        this.processingTimes.shift();
      }
      
      this.stats.avgProcessingTime = 
        this.processingTimes.reduce((a, b) => a + b, 0) / this.processingTimes.length;
    } else {
      this.stats.totalFailed++;
    }
  }
  
  getHealthStatus() {
    const successRate = (this.stats.totalProcessed / this.stats.totalReceived) * 100;
    const isHealthy = successRate > 95 && this.stats.avgProcessingTime < 1000;
    
    return {
      healthy: isHealthy,
      stats: this.stats,
      successRate: successRate.toFixed(2) + '%',
      alerts: this.generateAlerts()
    };
  }
  
  generateAlerts() {
    const alerts = [];
    const successRate = (this.stats.totalProcessed / this.stats.totalReceived) * 100;
    
    if (successRate < 90) {
      alerts.push({
        level: 'critical',
        message: `Low success rate: ${successRate.toFixed(2)}%`
      });
    }
    
    if (this.stats.avgProcessingTime > 5000) {
      alerts.push({
        level: 'warning',
        message: `High avg processing time: ${this.stats.avgProcessingTime}ms`
      });
    }
    
    if (this.stats.lastProcessed && Date.now() - this.stats.lastProcessed > 3600000) {
      alerts.push({
        level: 'warning',
        message: 'No webhooks processed in last hour'
      });
    }
    
    return alerts;
  }
}

const healthMonitor = new WebhookHealthMonitor();

// Health check endpoint
app.get('/webhooks/health', (req, res) => {
  const health = healthMonitor.getHealthStatus();
  res.status(health.healthy ? 200 : 503).json(health);
});

Error Notifications

class WebhookAlertSystem {
  constructor() {
    this.errorThresholds = {
      failureRate: 10, // 10% failure rate triggers alert
      consecutiveFailures: 5,
      avgProcessingTime: 5000 // 5 seconds
    };
    
    this.consecutiveFailures = 0;
    this.recentErrors = [];
  }
  
  async handleError(error, webhookData) {
    this.consecutiveFailures++;
    this.recentErrors.push({
      timestamp: new Date(),
      error: error.message,
      webhookId: webhookData.id,
      eventType: webhookData.eventSubCategory
    });
    
    // Keep only last 50 errors
    if (this.recentErrors.length > 50) {
      this.recentErrors = this.recentErrors.slice(-50);
    }
    
    // Check if we need to send alerts
    await this.checkAlertThresholds();
  }
  
  handleSuccess() {
    this.consecutiveFailures = 0;
  }
  
  async checkAlertThresholds() {
    if (this.consecutiveFailures >= this.errorThresholds.consecutiveFailures) {
      await this.sendAlert('critical', 
        `${this.consecutiveFailures} consecutive webhook failures`);
    }
    
    // Check failure rate in last 10 minutes
    const tenMinutesAgo = new Date(Date.now() - 10 * 60 * 1000);
    const recentErrors = this.recentErrors.filter(e => e.timestamp > tenMinutesAgo);
    
    if (recentErrors.length >= this.errorThresholds.failureRate) {
      await this.sendAlert('warning', 
        `High error rate: ${recentErrors.length} errors in last 10 minutes`);
    }
  }
  
  async sendAlert(level, message) {
    const alertData = {
      level,
      message,
      service: 'webhook-handler',
      timestamp: new Date().toISOString(),
      recentErrors: this.recentErrors.slice(-5) // Last 5 errors
    };
    
    // Send to multiple channels
    await Promise.all([
      this.sendSlackAlert(alertData),
      this.sendEmailAlert(alertData),
      this.logAlert(alertData)
    ]);
  }
  
  async sendSlackAlert(alertData) {
    // Implement Slack webhook notification
    console.log('Slack alert:', alertData);
  }
  
  async sendEmailAlert(alertData) {
    // Implement email notification
    console.log('Email alert:', alertData);
  }
  
  async logAlert(alertData) {
    console.error('WEBHOOK ALERT:', alertData);
  }
}

const alertSystem = new WebhookAlertSystem();

Performance Optimization

Async Processing

const Queue = require('bull');
const webhookQueue = new Queue('webhook processing');

// Add webhook to queue immediately
app.post('/webhooks/nudj', async (req, res) => {
  try {
    // Verify signature quickly
    if (!verifyWebhookSignature(req)) {
      return res.status(401).send('Unauthorized');
    }
    
    // Add to queue for async processing
    await webhookQueue.add('process', req.body, {
      attempts: 3,
      backoff: 'exponential',
      delay: 0
    });
    
    // Respond immediately
    res.status(200).send('OK');
    
  } catch (error) {
    console.error('Error queuing webhook:', error);
    res.status(500).send('Error');
  }
});

// Process webhooks from queue
webhookQueue.process('process', async (job) => {
  const webhookData = job.data;
  
  try {
    await processWebhookEvent(webhookData);
    console.log(`Processed webhook ${webhookData.id}`);
  } catch (error) {
    console.error(`Failed to process webhook ${webhookData.id}:`, error);
    throw error; // Will trigger retry
  }
});

Caching Strategies

const NodeCache = require('node-cache');
const cache = new NodeCache({ stdTTL: 300 }); // 5 minute TTL

async function processWebhookEvent(event) {
  const cacheKey = `user_${event.userId}`;
  
  // Try to get user from cache
  let user = cache.get(cacheKey);
  if (!user) {
    user = await getUserFromDatabase(event.userId);
    cache.set(cacheKey, user);
  }
  
  // Process with cached user data
  await processEventWithUser(event, user);
}

// Cache webhook configurations
async function getWebhookConfig(organisationId) {
  const cacheKey = `webhook_config_${organisationId}`;
  
  let config = cache.get(cacheKey);
  if (!config) {
    config = await fetchWebhookConfig(organisationId);
    cache.set(cacheKey, config, 600); // Cache for 10 minutes
  }
  
  return config;
}

Testing and Validation

Integration Testing

const request = require('supertest');
const app = require('../webhook-server');

describe('Webhook Integration Tests', () => {
  it('should process challenge completion webhook', async () => {
    const payload = {
      id: 'test-webhook-123',
      eventCategory: 'challenge',
      eventSubCategory: 'challenge-completion',
      userId: 'test-user-123',
      payload: {
        challengeName: 'Test Challenge',
        pointsEarned: 100
      }
    };
    
    const response = await request(app)
      .post('/webhooks/nudj')
      .set('Authorization', `Bearer ${process.env.WEBHOOK_SECRET}`)
      .set('X-Webhook-Source', 'nudj-platform')
      .send(payload)
      .expect(200);
    
    // Verify processing occurred
    const user = await getUserFromDatabase('test-user-123');
    expect(user.totalPointsEarned).toBeGreaterThan(0);
  });
  
  it('should reject invalid signatures', async () => {
    const payload = { id: 'test-webhook-invalid' };
    
    await request(app)
      .post('/webhooks/nudj')
      .set('Authorization', 'Bearer invalid-secret')
      .send(payload)
      .expect(401);
  });
  
  it('should handle duplicate webhooks', async () => {
    const payload = {
      id: 'duplicate-test-webhook',
      eventSubCategory: 'test-event'
    };
    
    // Send same webhook twice
    await request(app)
      .post('/webhooks/nudj')
      .set('Authorization', `Bearer ${process.env.WEBHOOK_SECRET}`)
      .set('X-Webhook-Source', 'nudj-platform')
      .send(payload)
      .expect(200);
    
    await request(app)
      .post('/webhooks/nudj')
      .set('Authorization', `Bearer ${process.env.WEBHOOK_SECRET}`)
      .set('X-Webhook-Source', 'nudj-platform')
      .send(payload)
      .expect(200);
    
    // Verify processing only happened once
    // Add your verification logic here
  });
});

Common Solutions

Network and Connectivity

Ensure your firewall allows incoming HTTPS traffic:
# Check if port 443 is open
sudo ufw status
sudo ufw allow 443

# For development (port 3000)
sudo ufw allow 3000
Configure your load balancer for webhook traffic:
  • Enable session affinity if needed
  • Set appropriate timeout values (>10 seconds)
  • Configure health checks
  • Ensure SSL termination is properly configured
Verify your domain configuration:
# Check DNS resolution
nslookup your-webhook-domain.com

# Test SSL certificate
openssl s_client -connect your-webhook-domain.com:443

# Verify certificate chain
curl -I https://your-webhook-domain.com/webhooks/nudj

Getting Help

Check the Logs

Start by examining your application logs and webhook processing logs for error patterns.

Test Locally

Use ngrok or similar tools to test webhooks against your local development environment.

Verify Configuration

Double-check your webhook configuration using the Admin API endpoints.

Contact Support

Reach out to support@nudj.cx with specific error messages and webhook IDs.
When contacting support, please include:
  • Webhook configuration (without secrets)
  • Specific error messages and stack traces
  • Webhook IDs that failed
  • Timeline of when issues started
  • Your endpoint URL (for connectivity testing)

Debugging Checklist

1

Verify Webhook Configuration

  • Webhook is enabled (isEnabled: true)
  • Events array contains expected event types
  • URL is valid and reachable
  • HTTP method is appropriate
  • Headers are correctly configured
2

Check Endpoint Implementation

  • Endpoint responds within 10 seconds
  • Returns HTTP 2xx for successful processing
  • Properly verifies webhook signatures
  • Handles JSON parsing errors
  • Implements idempotency
3

Test Network Connectivity

  • Endpoint is accessible from external networks
  • SSL certificate is valid
  • Firewall allows HTTPS traffic
  • DNS resolves correctly
4

Monitor and Log

  • Comprehensive logging is enabled
  • Error monitoring is configured
  • Health checks are implemented
  • Metrics are being tracked
Still having issues? Check our Implementation Guide or review the Event Catalog to ensure you’re handling webhooks correctly.