Overview
This guide walks you through implementing webhook endpoints to receive real-time notifications from the Nudj platform. You’ll learn how to configure webhooks, handle incoming requests, and implement best practices for production systems.Prerequisites
Admin API Access
You need Admin API credentials to configure webhooks
HTTPS Endpoint
Webhook URLs must use HTTPS in production
Event Processing
A system to handle and process incoming events
Authentication
Mechanism to verify webhook authenticity
Step 1: Configure Your Webhook
First, use the Admin API to configure your webhook endpoint:curl -X PUT "https://{subdomain}.nudj.cx/api/v2/admin/webhooks/configs" \
-H "Content-Type: application/json" \
-H "x-api-token: YOUR_ADMIN_API_TOKEN" \
-d '[
{
"events": [
"challenge-completion",
"achievement-completion",
"reward-claim"
],
"url": "https://your-app.com/webhooks/nudj",
"httpMethod": "POST",
"isEnabled": true,
"maxRetries": 3,
"headers": {
"Authorization": "Bearer your-webhook-secret",
"X-Webhook-Source": "nudj-platform"
}
}
]'
const response = await fetch(`https://${subdomain}.nudj.cx/api/v2/admin/webhooks/configs`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'x-api-token': process.env.NUDJ_ADMIN_API_TOKEN
},
body: JSON.stringify([
{
events: [
'challenge-completion',
'achievement-completion',
'reward-claim'
],
url: 'https://your-app.com/webhooks/nudj',
httpMethod: 'POST',
isEnabled: true,
maxRetries: 3,
headers: {
'Authorization': `Bearer ${process.env.WEBHOOK_SECRET}`,
'X-Webhook-Source': 'nudj-platform'
}
}
])
});
import requests
import os
response = requests.put(
f"https://{subdomain}.nudj.cx/api/v2/admin/webhooks/configs",
headers={
"Content-Type": "application/json",
"x-api-token": os.getenv("NUDJ_ADMIN_API_TOKEN")
},
json=[
{
"events": [
"challenge-completion",
"achievement-completion",
"reward-claim"
],
"url": "https://your-app.com/webhooks/nudj",
"httpMethod": "POST",
"isEnabled": True,
"maxRetries": 3,
"headers": {
"Authorization": f"Bearer {os.getenv('WEBHOOK_SECRET')}",
"X-Webhook-Source": "nudj-platform"
}
}
]
)
Step 2: Implement Webhook Endpoint
Create an endpoint to receive and process webhook events:const express = require('express');
const crypto = require('crypto');
const app = express();
// Middleware to parse JSON with raw body for signature verification
app.use('/webhooks', express.json({
verify: (req, res, buf, encoding) => {
req.rawBody = buf;
}
}));
// Webhook endpoint
app.post('/webhooks/nudj', async (req, res) => {
try {
// Verify the webhook is from Nudj
if (!verifyWebhookSignature(req)) {
return res.status(401).send('Unauthorized');
}
const event = req.body;
console.log('Received webhook:', event.eventSubCategory);
// Process the event
await processWebhookEvent(event);
// Always respond with 200 for successful processing
res.status(200).send('OK');
} catch (error) {
console.error('Webhook processing error:', error);
res.status(500).send('Internal Server Error');
}
});
function verifyWebhookSignature(req) {
const expectedAuth = `Bearer ${process.env.WEBHOOK_SECRET}`;
const receivedAuth = req.get('Authorization');
const webhookSource = req.get('X-Webhook-Source');
return receivedAuth === expectedAuth && webhookSource === 'nudj-platform';
}
async function processWebhookEvent(event) {
switch (event.eventSubCategory) {
case 'challenge-completion':
await handleChallengeCompletion(event);
break;
case 'achievement-completion':
await handleAchievementCompletion(event);
break;
case 'reward-claim':
await handleRewardClaim(event);
break;
default:
console.log('Unhandled event type:', event.eventSubCategory);
}
}
async function handleChallengeCompletion(event) {
const { userId, eventSourceId, payload } = event;
// Example: Send congratulatory email
await sendEmail(userId, {
subject: 'Challenge Completed!',
template: 'challenge-completion',
data: {
challengeName: payload.challengeName,
pointsEarned: payload.pointsEarned
}
});
// Example: Update external system
await updateExternalProfile(userId, {
lastChallengeCompleted: eventSourceId,
totalChallengesCompleted: await getChallengeCount(userId) + 1
});
}
app.listen(3000, () => {
console.log('Webhook server listening on port 3000');
});
from flask import Flask, request, jsonify
import os
import hashlib
import hmac
import json
app = Flask(__name__)
@app.route('/webhooks/nudj', methods=['POST'])
def handle_webhook():
try:
# Verify the webhook is from Nudj
if not verify_webhook_signature(request):
return jsonify({'error': 'Unauthorized'}), 401
event = request.json
print(f"Received webhook: {event.get('eventSubCategory')}")
# Process the event
process_webhook_event(event)
return jsonify({'status': 'success'}), 200
except Exception as e:
print(f"Webhook processing error: {e}")
return jsonify({'error': 'Internal Server Error'}), 500
def verify_webhook_signature(request):
expected_auth = f"Bearer {os.getenv('WEBHOOK_SECRET')}"
received_auth = request.headers.get('Authorization')
webhook_source = request.headers.get('X-Webhook-Source')
return (received_auth == expected_auth and
webhook_source == 'nudj-platform')
def process_webhook_event(event):
event_type = event.get('eventSubCategory')
handlers = {
'challenge-completion': handle_challenge_completion,
'achievement-completion': handle_achievement_completion,
'reward-claim': handle_reward_claim
}
handler = handlers.get(event_type)
if handler:
handler(event)
else:
print(f"Unhandled event type: {event_type}")
def handle_challenge_completion(event):
user_id = event.get('userId')
payload = event.get('payload', {})
# Example: Send congratulatory message
send_notification(user_id, {
'type': 'challenge_completed',
'message': f"Congratulations! You completed {payload.get('challengeName')}",
'points_earned': payload.get('pointsEarned')
})
# Example: Update user stats
update_user_stats(user_id, 'challenges_completed', 1)
def handle_achievement_completion(event):
# Implementation for achievement completion
pass
def handle_reward_claim(event):
# Implementation for reward claim
pass
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000)
require 'sinatra'
require 'json'
set :port, 4567
post '/webhooks/nudj' do
begin
# Verify the webhook is from Nudj
unless verify_webhook_signature(request)
halt 401, 'Unauthorized'
end
# Parse the JSON payload
event = JSON.parse(request.body.read)
puts "Received webhook: #{event['eventSubCategory']}"
# Process the event
process_webhook_event(event)
status 200
body 'OK'
rescue => e
puts "Webhook processing error: #{e.message}"
status 500
body 'Internal Server Error'
end
end
def verify_webhook_signature(request)
expected_auth = "Bearer #{ENV['WEBHOOK_SECRET']}"
received_auth = request.env['HTTP_AUTHORIZATION']
webhook_source = request.env['HTTP_X_WEBHOOK_SOURCE']
received_auth == expected_auth && webhook_source == 'nudj-platform'
end
def process_webhook_event(event)
case event['eventSubCategory']
when 'challenge-completion'
handle_challenge_completion(event)
when 'achievement-completion'
handle_achievement_completion(event)
when 'reward-claim'
handle_reward_claim(event)
else
puts "Unhandled event type: #{event['eventSubCategory']}"
end
end
def handle_challenge_completion(event)
user_id = event['userId']
payload = event['payload']
# Example: Send notification
send_notification(user_id, {
type: 'challenge_completed',
message: "You completed #{payload['challengeName']}!",
points_earned: payload['pointsEarned']
})
end
<?php
error_reporting(E_ALL);
ini_set('display_errors', 1);
// Get the raw POST data
$input = file_get_contents('php://input');
$event = json_decode($input, true);
// Verify webhook signature
if (!verifyWebhookSignature()) {
http_response_code(401);
die('Unauthorized');
}
try {
echo "Received webhook: " . $event['eventSubCategory'] . "\n";
// Process the event
processWebhookEvent($event);
http_response_code(200);
echo 'OK';
} catch (Exception $e) {
error_log("Webhook processing error: " . $e->getMessage());
http_response_code(500);
echo 'Internal Server Error';
}
function verifyWebhookSignature() {
$expectedAuth = 'Bearer ' . $_ENV['WEBHOOK_SECRET'];
$receivedAuth = $_SERVER['HTTP_AUTHORIZATION'] ?? '';
$webhookSource = $_SERVER['HTTP_X_WEBHOOK_SOURCE'] ?? '';
return $receivedAuth === $expectedAuth && $webhookSource === 'nudj-platform';
}
function processWebhookEvent($event) {
switch ($event['eventSubCategory']) {
case 'challenge-completion':
handleChallengeCompletion($event);
break;
case 'achievement-completion':
handleAchievementCompletion($event);
break;
case 'reward-claim':
handleRewardClaim($event);
break;
default:
error_log('Unhandled event type: ' . $event['eventSubCategory']);
}
}
function handleChallengeCompletion($event) {
$userId = $event['userId'];
$payload = $event['payload'];
// Example: Log the completion
error_log("User {$userId} completed challenge: " . $payload['challengeName']);
// Example: Send notification
sendNotification($userId, [
'type' => 'challenge_completed',
'message' => "Congratulations! You completed {$payload['challengeName']}",
'points_earned' => $payload['pointsEarned']
]);
}
function sendNotification($userId, $data) {
// Implementation for sending notifications
error_log("Sending notification to user {$userId}: " . json_encode($data));
}
?>
package main
import (
"encoding/json"
"fmt"
"log"
"net/http"
"os"
"github.com/gin-gonic/gin"
)
type WebhookEvent struct {
ID string `json:"id"`
EventCategory string `json:"eventCategory"`
EventSubCategory string `json:"eventSubCategory"`
OrganisationID string `json:"organisationId"`
CommunityID string `json:"communityId"`
UserID string `json:"userId"`
EventSourceID string `json:"eventSourceId"`
Payload interface{} `json:"payload"`
Tags []string `json:"tags"`
CreatedAt string `json:"createdAt"`
}
func main() {
r := gin.Default()
r.POST("/webhooks/nudj", handleWebhook)
log.Println("Webhook server starting on :8080")
r.Run(":8080")
}
func handleWebhook(c *gin.Context) {
// Verify webhook signature
if !verifyWebhookSignature(c) {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
return
}
var event WebhookEvent
if err := c.ShouldBindJSON(&event); err != nil {
log.Printf("Error parsing webhook: %v", err)
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid JSON"})
return
}
log.Printf("Received webhook: %s", event.EventSubCategory)
// Process the event
if err := processWebhookEvent(event); err != nil {
log.Printf("Error processing webhook: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Processing failed"})
return
}
c.JSON(http.StatusOK, gin.H{"status": "success"})
}
func verifyWebhookSignature(c *gin.Context) bool {
expectedAuth := fmt.Sprintf("Bearer %s", os.Getenv("WEBHOOK_SECRET"))
receivedAuth := c.GetHeader("Authorization")
webhookSource := c.GetHeader("X-Webhook-Source")
return receivedAuth == expectedAuth && webhookSource == "nudj-platform"
}
func processWebhookEvent(event WebhookEvent) error {
switch event.EventSubCategory {
case "challenge-completion":
return handleChallengeCompletion(event)
case "achievement-completion":
return handleAchievementCompletion(event)
case "reward-claim":
return handleRewardClaim(event)
default:
log.Printf("Unhandled event type: %s", event.EventSubCategory)
return nil
}
}
func handleChallengeCompletion(event WebhookEvent) error {
payload, _ := json.Marshal(event.Payload)
log.Printf("User %s completed a challenge: %s", event.UserID, string(payload))
// Add your business logic here
// - Send notifications
// - Update external systems
// - Trigger other processes
return nil
}
func handleAchievementCompletion(event WebhookEvent) error {
// Implementation for achievement completion
return nil
}
func handleRewardClaim(event WebhookEvent) error {
// Implementation for reward claim
return nil
}
Step 3: Implement Event Handlers
Create specific handlers for different event types:async function handleChallengeCompletion(event) {
const { userId, communityId, eventSourceId, payload } = event;
const challengeId = eventSourceId;
try {
// 1. Award bonus points for fast completion
if (payload.timeToComplete < 3600) { // Less than 1 hour
await awardBonusPoints(userId, 50, 'Fast challenge completion');
}
// 2. Update user statistics
await updateUserStats(userId, {
challengesCompleted: 1,
totalPointsEarned: payload.pointsEarned,
totalXpEarned: payload.xpEarned
});
// 3. Check for achievement eligibility
const challengeCount = await getUserChallengeCount(userId);
if (challengeCount === 10) {
await triggerAchievement(userId, 'challenge-master');
}
// 4. Send personalized notification
await sendNotification(userId, {
type: 'challenge_completed',
title: 'Challenge Complete!',
message: `Great job completing "${payload.challengeName}"!`,
data: {
pointsEarned: payload.pointsEarned,
xpEarned: payload.xpEarned,
completionTime: formatDuration(payload.timeToComplete)
}
});
// 5. Update leaderboards
await updateLeaderboard(communityId, userId, {
challengesCompleted: 1,
pointsEarned: payload.pointsEarned
});
} catch (error) {
console.error(`Error handling challenge completion for user ${userId}:`, error);
throw error;
}
}
async function handleRewardClaim(event) {
const { userId, eventSourceId, payload } = event;
const rewardId = eventSourceId;
try {
// 1. Log the reward claim for analytics
await logEvent('reward_claimed', {
userId,
rewardId,
rewardName: payload.rewardName,
rewardType: payload.rewardType,
timestamp: event.createdAt
});
// 2. Handle different reward types
switch (payload.rewardType) {
case 'asset':
await handleAssetReward(userId, rewardId, payload);
break;
case 'entry':
await handleEntryReward(userId, rewardId, payload);
break;
default:
console.warn('Unknown reward type:', payload.rewardType);
}
// 3. Send confirmation notification
await sendNotification(userId, {
type: 'reward_claimed',
title: 'Reward Claimed!',
message: `You've successfully claimed: ${payload.rewardName}`,
data: payload
});
// 4. Update user profile with reward
await updateUserProfile(userId, {
rewardsClaimed: 1,
lastRewardClaimed: {
id: rewardId,
name: payload.rewardName,
claimedAt: event.createdAt
}
});
} catch (error) {
console.error(`Error handling reward claim for user ${userId}:`, error);
throw error;
}
}
async function handleAssetReward(userId, rewardId, payload) {
// Asset rewards are permanent items (badges, titles, etc.)
await addUserAsset(userId, {
rewardId,
name: payload.rewardName,
type: 'reward_asset',
acquiredAt: new Date().toISOString()
});
}
async function handleEntryReward(userId, rewardId, payload) {
// Entry rewards are chances to win (lottery entries, etc.)
await addUserEntry(userId, {
rewardId,
name: payload.rewardName,
type: 'reward_entry',
enteredAt: new Date().toISOString()
});
}
async function handleVariableCaptured(event) {
const { userId, payload } = event;
try {
// 1. Store the captured variable
await storeUserVariable(userId, {
variableId: payload.variableId,
variableName: payload.variableName,
value: payload.value,
capturedAt: payload.capturedAt
});
// 2. Trigger personalization logic
await updateUserPersonalization(userId, {
[payload.variableName]: payload.value
});
// 3. Check for completion of user profile
const profileCompletion = await calculateProfileCompletion(userId);
if (profileCompletion === 100) {
await triggerAchievement(userId, 'profile-complete');
}
// 4. Update segmentation
await updateUserSegmentation(userId, {
variableName: payload.variableName,
value: payload.value
});
} catch (error) {
console.error(`Error handling variable capture for user ${userId}:`, error);
throw error;
}
}
Step 4: Error Handling and Retry Logic
Implement robust error handling to ensure reliable webhook processing:const MAX_RETRIES = 3;
const RETRY_DELAY = 1000; // 1 second
async function processWebhookEvent(event) {
let attempt = 1;
while (attempt <= MAX_RETRIES) {
try {
await processEvent(event);
return; // Success, exit retry loop
} catch (error) {
console.error(`Attempt ${attempt} failed:`, error);
if (attempt === MAX_RETRIES) {
// Final attempt failed, handle accordingly
await handleFinalFailure(event, error);
throw error;
}
// Wait before retrying
await sleep(RETRY_DELAY * attempt);
attempt++;
}
}
}
async function handleFinalFailure(event, error) {
// Log to error monitoring service
console.error('Webhook processing failed after all retries:', {
eventId: event.id,
eventType: event.eventSubCategory,
error: error.message,
stack: error.stack
});
// Store in dead letter queue for manual review
await storeInDeadLetterQueue(event, error);
// Alert operations team
await sendAlert('webhook-processing-failed', {
eventId: event.id,
eventType: event.eventSubCategory,
error: error.message
});
}
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
// Store processed webhook IDs to prevent duplicate processing
const processedWebhooks = new Set();
app.post('/webhooks/nudj', async (req, res) => {
try {
const event = req.body;
const webhookId = event.id;
// Check if we've already processed this webhook
if (processedWebhooks.has(webhookId)) {
console.log(`Webhook ${webhookId} already processed, ignoring`);
return res.status(200).send('OK');
}
// Verify the webhook
if (!verifyWebhookSignature(req)) {
return res.status(401).send('Unauthorized');
}
// Process the event
await processWebhookEvent(event);
// Mark as processed
processedWebhooks.add(webhookId);
res.status(200).send('OK');
} catch (error) {
console.error('Webhook processing error:', error);
res.status(500).send('Internal Server Error');
}
});
Step 5: Testing Your Implementation
Test your webhook implementation thoroughly:const axios = require('axios');
// Test webhook endpoint locally
async function testWebhook() {
const testPayload = {
id: 'test-webhook-' + Date.now(),
eventCategory: 'challenge',
eventSubCategory: 'challenge-completion',
organisationId: '60f7b8c8e7d4a2b1c9e3f4a1',
communityId: '60f7b8c8e7d4a2b1c9e3f4a2',
userId: '60f7b8c8e7d4a2b1c9e3f4a3',
eventSourceId: '60f7b8c8e7d4a2b1c9e3f4a4',
payload: {
challengeId: '60f7b8c8e7d4a2b1c9e3f4a4',
challengeName: 'Test Challenge',
completedAt: new Date().toISOString(),
timeToComplete: 1800,
pointsEarned: 100,
xpEarned: 50
},
createdAt: new Date().toISOString()
};
try {
const response = await axios.post('http://localhost:3000/webhooks/nudj', testPayload, {
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${process.env.WEBHOOK_SECRET}`,
'X-Webhook-Source': 'nudj-platform'
}
});
console.log('Test webhook successful:', response.status);
} catch (error) {
console.error('Test webhook failed:', error.message);
}
}
testWebhook();
# Install ngrok for local testing
npm install -g ngrok
# Start your local webhook server
node webhook-server.js &
# Expose local server via ngrok
ngrok http 3000
# Use the ngrok URL in your webhook configuration
# Example: https://abc123.ngrok.io/webhooks/nudj
Step 6: Production Deployment
Security Checklist
HTTPS Configuration
HTTPS Configuration
- Use valid SSL certificates
- Enable HSTS headers
- Disable HTTP fallback
- Use strong cipher suites
Authentication & Authorization
Authentication & Authorization
- Verify webhook signatures
- Use environment variables for secrets
- Implement rate limiting
- Add IP allowlisting if needed
Error Handling
Error Handling
- Implement retry logic
- Use dead letter queues
- Add comprehensive logging
- Set up monitoring and alerts
Performance
Performance
- Process webhooks asynchronously
- Implement connection pooling
- Add caching where appropriate
- Monitor response times
Monitoring and Logging
const winston = require('winston');
const logger = winston.createLogger({
level: 'info',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.errors({ stack: true }),
winston.format.json()
),
defaultMeta: { service: 'webhook-handler' },
transports: [
new winston.transports.File({ filename: 'error.log', level: 'error' }),
new winston.transports.File({ filename: 'combined.log' }),
new winston.transports.Console({
format: winston.format.simple()
})
]
});
app.post('/webhooks/nudj', async (req, res) => {
const startTime = Date.now();
const event = req.body;
logger.info('Webhook received', {
webhookId: event.id,
eventType: event.eventSubCategory,
userId: event.userId,
timestamp: event.createdAt
});
try {
await processWebhookEvent(event);
logger.info('Webhook processed successfully', {
webhookId: event.id,
processingTime: Date.now() - startTime
});
res.status(200).send('OK');
} catch (error) {
logger.error('Webhook processing failed', {
webhookId: event.id,
error: error.message,
stack: error.stack,
processingTime: Date.now() - startTime
});
res.status(500).send('Internal Server Error');
}
});
app.get('/health', (req, res) => {
res.status(200).json({
status: 'healthy',
timestamp: new Date().toISOString(),
uptime: process.uptime(),
version: process.env.APP_VERSION || '1.0.0'
});
});
app.get('/webhooks/health', (req, res) => {
// Check if webhook processing is healthy
const isHealthy = checkWebhookHealth();
res.status(isHealthy ? 200 : 503).json({
status: isHealthy ? 'healthy' : 'unhealthy',
webhookProcessor: isHealthy,
lastProcessedWebhook: getLastProcessedWebhookTime(),
timestamp: new Date().toISOString()
});
});
Common Integration Patterns
Real-time Notifications
async function sendPushNotification(userId, event) {
const userDevices = await getUserDevices(userId);
const notification = {
title: getNotificationTitle(event),
body: getNotificationMessage(event),
data: {
eventType: event.eventSubCategory,
eventId: event.id,
deepLink: generateDeepLink(event)
}
};
for (const device of userDevices) {
await pushService.send(device.token, notification);
}
}
function getNotificationTitle(event) {
const titles = {
'challenge-completion': 'Challenge Complete! 🏆',
'achievement-completion': 'Achievement Unlocked! 🎉',
'reward-claim': 'Reward Claimed! 🎁',
'streak-extended': 'Streak Extended! 🔥'
};
return titles[event.eventSubCategory] || 'Nudj Notification';
}
Email Marketing Integration
async function triggerEmailCampaign(event) {
const user = await getUser(event.userId);
const campaignData = {
email: user.email,
templateId: getEmailTemplate(event.eventSubCategory),
personalizations: [{
email: user.email,
dynamic_template_data: {
firstName: user.firstName,
eventData: event.payload,
communityName: await getCommunityName(event.communityId)
}
}]
};
await emailService.send(campaignData);
}
function getEmailTemplate(eventType) {
const templates = {
'challenge-completion': 'challenge_completed_v2',
'achievement-completion': 'achievement_unlocked_v2',
'reward-claim': 'reward_claimed_v1'
};
return templates[eventType] || 'generic_notification_v1';
}
Analytics Integration
async function trackAnalyticsEvent(event) {
const analyticsEvent = {
userId: event.userId,
eventName: `nudj_${event.eventSubCategory}`,
properties: {
organisationId: event.organisationId,
communityId: event.communityId,
eventSourceId: event.eventSourceId,
timestamp: event.createdAt,
...event.payload
}
};
// Send to multiple analytics platforms
await Promise.all([
analytics.track(analyticsEvent),
mixpanel.track(analyticsEvent.userId, analyticsEvent.eventName, analyticsEvent.properties),
amplitude.logEvent(analyticsEvent)
]);
}
Best Practices
Response Quickly
Return HTTP 200 within 10 seconds. Process heavy workloads asynchronously.
Handle Duplicates
Use webhook IDs to implement idempotent processing and prevent duplicate handling.
Validate Payloads
Always verify webhook authenticity and validate payload structure before processing.
Monitor Performance
Track processing times, error rates, and webhook delivery success rates.
Implement Retries
Handle transient failures gracefully with exponential backoff retry logic.
Log Everything
Maintain detailed logs for debugging and monitoring webhook processing.
Next Steps
Event Catalog
See every available webhook event and its specific payload.
Testing Guide
Learn how to trigger test events to verify your endpoint.

