Skip to main content
Learn how to set up and use webhooks to receive real-time notifications when events happen in DRIP realms. Perfect for building responsive applications and automations.

Overview

Webhooks allow your application to receive instant notifications when specific events occur in DRIP, such as:
  • Member joins or leaves
  • Points are awarded or spent
  • Quests are completed
  • Store purchases are made
  • Custom events from other apps
Webhooks are HTTP POST requests sent to your server whenever events occur. They’re much more efficient than polling the API repeatedly.

Setting Up Webhooks

1. Create a Webhook Endpoint

First, create an endpoint in your application to receive webhook notifications:
const express = require('express');
const crypto = require('crypto');

const app = express();
app.use(express.json());

// Webhook endpoint
app.post('/webhooks/drip', (req, res) => {
  const signature = req.headers['x-drip-signature'];
  const payload = JSON.stringify(req.body);
  
  // Verify webhook signature (recommended)
  if (!verifySignature(payload, signature)) {
    return res.status(401).send('Unauthorized');
  }
  
  // Process the webhook event
  handleWebhookEvent(req.body);
  
  // Always respond with 200 OK
  res.status(200).send('OK');
});

function verifySignature(payload, signature) {
  const expectedSignature = crypto
    .createHmac('sha256', process.env.WEBHOOK_SECRET)
    .update(payload)
    .digest('hex');
  
  return signature === `sha256=${expectedSignature}`;
}

function handleWebhookEvent(event) {
  console.log('Received webhook:', event.type);
  
  switch (event.type) {
    case 'member.points.awarded':
      handlePointsAwarded(event.data);
      break;
    case 'member.joined':
      handleMemberJoined(event.data);
      break;
    case 'quest.completed':
      handleQuestCompleted(event.data);
      break;
    default:
      console.log('Unhandled event type:', event.type);
  }
}

app.listen(3000, () => {
  console.log('Webhook server running on port 3000');
});

2. Register Your Webhook

Register your webhook endpoint with DRIP:
async function registerWebhook(client, webhookUrl, events) {
  const webhookData = {
    url: webhookUrl,
    events: events,
    secret: process.env.WEBHOOK_SECRET // Optional but recommended
  };
  
  const result = await client.request('POST', 
    `/realm/${client.realmId}/webhooks`,
    webhookData
  );
  
  console.log('Webhook registered:', result.id);
  return result;
}

// Register webhook for specific events
const webhook = await registerWebhook(client, 'https://yourapp.com/webhooks/drip', [
  'member.joined',
  'member.points.awarded',
  'quest.completed',
  'store.purchase.completed'
]);

Available Events

Member Events

Triggered when a new member joins the realm.
{
  "type": "member.joined",
  "timestamp": "2024-01-20T15:30:00Z",
  "realmId": "507f1f77bcf86cd799439011",
  "data": {
    "member": {
      "id": "507f1f77bcf86cd799439013",
      "username": "newmember123",
      "displayName": "New Member",
      "joinedAt": "2024-01-20T15:30:00Z",
      "credentials": [
        {
          "type": "discord",
          "platformId": "123456789012345678"
        }
      ]
    }
  }
}
Triggered when a member leaves the realm.
{
  "type": "member.left",
  "timestamp": "2024-01-20T16:00:00Z",
  "realmId": "507f1f77bcf86cd799439011",
  "data": {
    "memberId": "507f1f77bcf86cd799439013",
    "leftAt": "2024-01-20T16:00:00Z"
  }
}
Triggered when points are awarded to a member.
{
  "type": "member.points.awarded",
  "timestamp": "2024-01-20T15:45:00Z",
  "realmId": "507f1f77bcf86cd799439011",
  "data": {
    "member": {
      "id": "507f1f77bcf86cd799439013",
      "username": "member123"
    },
    "points": {
      "amount": 100,
      "realmPoint": {
        "id": "507f1f77bcf86cd799439015",
        "name": "XP",
        "emoji": "⭐"
      }
    },
    "reason": "Completed daily quest",
    "newBalance": 1250
  }
}

Quest Events

Triggered when a member completes a quest.
{
  "type": "quest.completed",
  "timestamp": "2024-01-20T16:15:00Z",
  "realmId": "507f1f77bcf86cd799439011",
  "data": {
    "quest": {
      "id": "507f1f77bcf86cd799439020",
      "name": "Daily Engagement",
      "type": "daily"
    },
    "member": {
      "id": "507f1f77bcf86cd799439013",
      "username": "member123"
    },
    "completedAt": "2024-01-20T16:15:00Z",
    "rewards": [
      {
        "type": "points",
        "amount": 100,
        "realmPointId": "507f1f77bcf86cd799439015"
      }
    ]
  }
}
Triggered when a member starts a quest.
{
  "type": "quest.started",
  "timestamp": "2024-01-20T14:00:00Z",
  "realmId": "507f1f77bcf86cd799439011",
  "data": {
    "quest": {
      "id": "507f1f77bcf86cd799439020",
      "name": "Weekly Challenge"
    },
    "member": {
      "id": "507f1f77bcf86cd799439013",
      "username": "member123"
    },
    "startedAt": "2024-01-20T14:00:00Z"
  }
}

Store Events

Triggered when a member completes a store purchase.
{
  "type": "store.purchase.completed",
  "timestamp": "2024-01-20T17:00:00Z",
  "realmId": "507f1f77bcf86cd799439011",
  "data": {
    "purchase": {
      "id": "507f1f77bcf86cd799439025",
      "item": {
        "id": "507f1f77bcf86cd799439030",
        "name": "VIP Role",
        "cost": 1000
      },
      "member": {
        "id": "507f1f77bcf86cd799439013",
        "username": "member123"
      },
      "completedAt": "2024-01-20T17:00:00Z"
    }
  }
}

Event Handlers

Example Event Handlers

async function handlePointsAwarded(data) {
  const { member, points, reason, newBalance } = data;
  
  console.log(`πŸŽ‰ ${member.username} earned ${points.amount} ${points.realmPoint.emoji}`);
  console.log(`Reason: ${reason}`);
  console.log(`New balance: ${newBalance}`);
  
  // Custom logic - maybe update a leaderboard
  await updateLeaderboard(member.id, newBalance);
  
  // Send congratulations message
  if (points.amount >= 100) {
    await sendCongratulationsMessage(member.id, points.amount);
  }
}

async function handleMemberJoined(data) {
  const { member } = data;
  
  console.log(`πŸ‘‹ Welcome ${member.displayName}!`);
  
  // Give welcome bonus
  await awardWelcomeBonus(member.id);
  
  // Add to mailing list
  await addToMailingList(member.username);
  
  // Send welcome message
  await sendWelcomeMessage(member.id);
}

async function handleQuestCompleted(data) {
  const { quest, member, rewards } = data;
  
  console.log(`βœ… ${member.username} completed: ${quest.name}`);
  
  // Track quest completion analytics
  await trackQuestCompletion(quest.id, member.id);
  
  // Check for quest streaks
  const streak = await getQuestStreak(member.id);
  if (streak >= 7) {
    await awardStreakBonus(member.id, streak);
  }
}

Advanced Webhook Patterns

Webhook Queue Processing

For high-volume applications, implement queue processing:
const Queue = require('bull');
const webhookQueue = new Queue('webhook processing');

// Add webhook to queue instead of processing immediately
app.post('/webhooks/drip', (req, res) => {
  // Verify signature first
  if (!verifySignature(req.body, req.headers['x-drip-signature'])) {
    return res.status(401).send('Unauthorized');
  }
  
  // Add to queue for processing
  webhookQueue.add('process-webhook', req.body, {
    attempts: 3,
    backoff: 'exponential'
  });
  
  res.status(200).send('OK');
});

// Process webhooks from queue
webhookQueue.process('process-webhook', async (job) => {
  const event = job.data;
  
  try {
    await handleWebhookEvent(event);
    console.log(`βœ… Processed webhook: ${event.type}`);
  } catch (error) {
    console.error(`❌ Failed to process webhook: ${error.message}`);
    throw error; // Will trigger retry
  }
});

Idempotency Handling

Prevent duplicate processing of webhook events:
const processedEvents = new Set();

function isEventProcessed(eventId) {
  return processedEvents.has(eventId);
}

function markEventProcessed(eventId) {
  processedEvents.add(eventId);
  
  // Clean up old event IDs (keep last 10,000)
  if (processedEvents.size > 10000) {
    const oldestEvents = Array.from(processedEvents).slice(0, 1000);
    oldestEvents.forEach(id => processedEvents.delete(id));
  }
}

async function handleWebhookEvent(event) {
  const eventId = event.id || `${event.type}-${event.timestamp}`;
  
  if (isEventProcessed(eventId)) {
    console.log(`⏭️  Skipping duplicate event: ${eventId}`);
    return;
  }
  
  // Process the event
  await processEvent(event);
  
  // Mark as processed
  markEventProcessed(eventId);
}

Webhook Retry Logic

Handle webhook failures gracefully:
class WebhookProcessor {
  constructor() {
    this.maxRetries = 3;
    this.retryDelay = 1000; // 1 second
  }
  
  async processWithRetry(event, attempt = 1) {
    try {
      await this.processEvent(event);
      console.log(`βœ… Processed webhook on attempt ${attempt}`);
    } catch (error) {
      console.error(`❌ Attempt ${attempt} failed: ${error.message}`);
      
      if (attempt < this.maxRetries) {
        const delay = this.retryDelay * Math.pow(2, attempt - 1); // Exponential backoff
        console.log(`⏳ Retrying in ${delay}ms...`);
        
        setTimeout(() => {
          this.processWithRetry(event, attempt + 1);
        }, delay);
      } else {
        console.error(`πŸ’€ Failed after ${this.maxRetries} attempts`);
        await this.handleFailedWebhook(event, error);
      }
    }
  }
  
  async handleFailedWebhook(event, error) {
    // Log to error tracking service
    console.error('Webhook processing failed permanently:', {
      event: event.type,
      error: error.message,
      timestamp: event.timestamp
    });
    
    // Maybe store in database for manual review
    await this.storeFailedWebhook(event, error);
  }
}

Testing Webhooks

Local Testing with ngrok

For local development, use ngrok to expose your local server:
# Install ngrok
npm install -g ngrok

# Expose your local server
ngrok http 3000

# Use the ngrok URL for webhook registration
# https://abc123.ngrok.io/webhooks/drip

Webhook Testing Tools

Create a test webhook handler:
// test-webhook.js
const express = require('express');
const app = express();

app.use(express.json());

app.post('/webhooks/drip', (req, res) => {
  console.log('πŸ“¦ Received webhook:');
  console.log('Type:', req.body.type);
  console.log('Timestamp:', req.body.timestamp);
  console.log('Data:', JSON.stringify(req.body.data, null, 2));
  console.log('---');
  
  res.status(200).send('OK');
});

app.listen(3000, () => {
  console.log('πŸ” Webhook test server running on http://localhost:3000');
  console.log('πŸ“ Register webhook URL: http://localhost:3000/webhooks/drip');
});

Manual Webhook Testing

Test your webhook endpoint manually:
# Test your webhook endpoint
curl -X POST "http://localhost:3000/webhooks/drip" \
  -H "Content-Type: application/json" \
  -H "X-Drip-Signature: sha256=test" \
  -d '{
    "type": "member.points.awarded",
    "timestamp": "2024-01-20T15:30:00Z",
    "realmId": "test-realm",
    "data": {
      "member": {
        "id": "test-member",
        "username": "testuser"
      },
      "points": {
        "amount": 100,
        "realmPoint": {
          "name": "XP",
          "emoji": "⭐"
        }
      },
      "reason": "Test webhook",
      "newBalance": 1000
    }
  }'

Webhook Management

List Registered Webhooks

async function listWebhooks(client) {
  const webhooks = await client.request('GET', 
    `/realm/${client.realmId}/webhooks`
  );
  
  console.log('Registered webhooks:');
  webhooks.data.forEach(webhook => {
    console.log(`- ${webhook.id}: ${webhook.url}`);
    console.log(`  Events: ${webhook.events.join(', ')}`);
    console.log(`  Active: ${webhook.active}`);
  });
  
  return webhooks.data;
}

Update Webhook

async function updateWebhook(client, webhookId, updates) {
  const result = await client.request('PATCH',
    `/realm/${client.realmId}/webhooks/${webhookId}`,
    updates
  );
  
  console.log('Webhook updated:', result);
  return result;
}

// Example: Add new events to existing webhook
await updateWebhook(client, 'webhook-id', {
  events: ['member.joined', 'member.points.awarded', 'quest.completed', 'store.purchase.completed']
});

Delete Webhook

async function deleteWebhook(client, webhookId) {
  await client.request('DELETE',
    `/realm/${client.realmId}/webhooks/${webhookId}`
  );
  
  console.log('Webhook deleted:', webhookId);
}

Security Best Practices

Signature Verification

Always verify webhook signatures to ensure requests are from DRIP:
function verifySignature(payload, signature) {
  const expectedSig = crypto
    .createHmac('sha256', process.env.WEBHOOK_SECRET)
    .update(payload)
    .digest('hex');
  
  return signature === `sha256=${expectedSig}`;
}

HTTPS Only

Only register webhook URLs that use HTTPS in production:
function validateWebhookUrl(url) {
  if (!url.startsWith('https://')) {
    throw new Error('Webhook URLs must use HTTPS');
  }
  return true;
}

Rate Limiting

Implement rate limiting on your webhook endpoint:
const rateLimit = require('express-rate-limit');

const webhookLimiter = rateLimit({
  windowMs: 1 * 60 * 1000, // 1 minute
  max: 100, // 100 requests per minute
  message: 'Too many webhook requests'
});

app.use('/webhooks/drip', webhookLimiter);

Error Handling

Handle errors gracefully to prevent webhook delivery failures:
app.post('/webhooks/drip', async (req, res) => {
  try {
    await handleWebhookEvent(req.body);
    res.status(200).send('OK');
  } catch (error) {
    console.error('Webhook error:', error);
    // Still return 200 to prevent retries for unrecoverable errors
    res.status(200).send('Error logged');
  }
});

Troubleshooting

Possible causes:
  • Webhook URL is not accessible from the internet
  • Signature verification is failing
  • Your server is returning non-200 status codes
Solutions:
  • Test webhook URL with curl or online tools
  • Check signature verification logic
  • Always return 200 OK from webhook endpoint
  • Use ngrok for local testing
Possible causes:
  • Network issues causing retries
  • Multiple webhook registrations
  • Not implementing idempotency
Solutions:
  • Implement event deduplication using event IDs
  • Check for multiple webhook registrations
  • Store processed event IDs to prevent duplicates
Possible causes:
  • Webhook endpoint is down
  • Events not subscribed to
  • Processing errors causing DRIP to stop sending
Solutions:
  • Ensure webhook endpoint has high uptime
  • Check subscribed event types
  • Always return 200 OK even for processing errors
  • Implement proper error handling

Next Steps

Need help with webhooks? Join our Discord community to get support from other developers and share your webhook implementations! πŸ””