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:
Node.js/Express
Python/Flask
Next.js API Route
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' );
});
from flask import Flask, request, jsonify
import hmac
import hashlib
import json
import os
app = Flask( __name__ )
@app.route ( '/webhooks/drip' , methods =[ 'POST' ])
def webhook ():
signature = request.headers.get( 'X-Drip-Signature' )
payload = request.get_data( as_text = True )
# Verify webhook signature
if not verify_signature(payload, signature):
return 'Unauthorized' , 401
# Process the webhook event
event = request.json
handle_webhook_event(event)
return 'OK' , 200
def verify_signature ( payload , signature ):
secret = os.getenv( 'WEBHOOK_SECRET' ).encode( 'utf-8' )
expected_signature = hmac.new(
secret,
payload.encode( 'utf-8' ),
hashlib.sha256
).hexdigest()
return signature == f "sha256= { expected_signature } "
def handle_webhook_event ( event ):
print ( f "Received webhook: { event[ 'type' ] } " )
event_type = event[ 'type' ]
if event_type == 'member.points.awarded' :
handle_points_awarded(event[ 'data' ])
elif event_type == 'member.joined' :
handle_member_joined(event[ 'data' ])
elif event_type == 'quest.completed' :
handle_quest_completed(event[ 'data' ])
else :
print ( f "Unhandled event type: { event_type } " )
if __name__ == '__main__' :
app.run( port = 3000 )
// pages/api/webhooks/drip.js
import crypto from 'crypto' ;
export default async function handler ( req , res ) {
if ( req . method !== 'POST' ) {
return res . status ( 405 ). json ({ error: 'Method not allowed' });
}
const signature = req . headers [ 'x-drip-signature' ];
const payload = JSON . stringify ( req . body );
// Verify signature
if (! verifySignature ( payload , signature )) {
return res . status ( 401 ). json ({ error: 'Unauthorized' });
}
// Process webhook
await handleWebhookEvent ( req . body );
res . status ( 200 ). json ({ received: true });
}
function verifySignature ( payload , signature ) {
const expectedSignature = crypto
. createHmac ( 'sha256' , process . env . WEBHOOK_SECRET )
. update ( payload )
. digest ( 'hex' );
return signature === `sha256= ${ expectedSignature } ` ;
}
async function handleWebhookEvent ( event ) {
console . log ( 'Webhook received:' , event . type );
// Handle different event types
switch ( event . type ) {
case 'member.points.awarded' :
await handlePointsAwarded ( event . data );
break ;
case 'member.joined' :
await handleMemberJoined ( event . data );
break ;
default :
console . log ( 'Unhandled event:' , event . type );
}
}
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'
]);
def register_webhook ( client , webhook_url , events ):
webhook_data = {
'url' : webhook_url,
'events' : events,
'secret' : os.getenv( 'WEBHOOK_SECRET' )
}
result = client.request( 'POST' ,
f '/realm/ { client.realm_id } /webhooks' ,
webhook_data
)
print ( f "Webhook registered: { result[ 'id' ] } " )
return result
# Register webhook
webhook = register_webhook(client, 'https://yourapp.com/webhooks/drip' , [
'member.joined' ,
'member.points.awarded' ,
'quest.completed' ,
'store.purchase.completed'
])
curl -X POST "https://api.drip.re/api/v1/realm/YOUR_REALM_ID/webhooks" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://yourapp.com/webhooks/drip",
"events": [
"member.joined",
"member.points.awarded",
"quest.completed"
],
"secret": "your_webhook_secret"
}'
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 );
}
}
async def handle_points_awarded ( data ):
member = data[ 'member' ]
points = data[ 'points' ]
reason = data[ 'reason' ]
new_balance = data[ 'newBalance' ]
print ( f "π { member[ 'username' ] } earned { points[ 'amount' ] } { points[ 'realmPoint' ][ 'emoji' ] } " )
print ( f "Reason: { reason } " )
print ( f "New balance: { new_balance } " )
# Update leaderboard
await update_leaderboard(member[ 'id' ], new_balance)
# Send congratulations for large awards
if points[ 'amount' ] >= 100 :
await send_congratulations_message(member[ 'id' ], points[ 'amount' ])
async def handle_member_joined ( data ):
member = data[ 'member' ]
print ( f "π Welcome { member[ 'displayName' ] } !" )
# Give welcome bonus
await award_welcome_bonus(member[ 'id' ])
# Send welcome message
await send_welcome_message(member[ 'id' ])
async def handle_quest_completed ( data ):
quest = data[ 'quest' ]
member = data[ 'member' ]
rewards = data[ 'rewards' ]
print ( f "β
{ member[ 'username' ] } completed: { quest[ 'name' ] } " )
# Track analytics
await track_quest_completion(quest[ 'id' ], member[ 'id' ])
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
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
Webhook Not Receiving Events
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! π