Webhooks Integration Guide
Automate your workflows with real-time webhook notifications. 🔔
Table of Contents
- Overview
- Setup
- Payload Structure
- Signature Verification
- Retry Logic
- Integration Examples
- Troubleshooting
Overview
Webhooks allow your server to receive real-time notifications when events occur in your Waitlist Widget account.
Supported Events
| Event | Trigger | Available Plans |
|---|---|---|
subscriber.created | New subscriber joins waitlist | Starter, Pro |
subscriber.deleted | Subscriber removed | Starter, Pro |
project.created | New project created | Pro |
project.updated | Project settings updated | Pro |
project.deleted | Project deleted | Pro |
⚠️ Note: Webhook feature is available on Starter ($9/mo) and Pro ($19/mo) plans only.
Setup
Step 1: Create Webhook Endpoint
Create an endpoint on your server to receive webhook events:
// Express.js example
const express = require('express');
const crypto = require('crypto');
const app = express();
app.use(express.json());
app.post('/webhooks/waitlist', async (req, res) => {
const signature = req.headers['x-webhook-signature'];
const payload = JSON.stringify(req.body);
// Verify signature (see Signature Verification section)
if (!verifySignature(payload, signature, process.env.WEBHOOK_SECRET)) {
return res.status(401).json({ error: 'Invalid signature' });
}
// Process the event
const { event, data } = req.body;
console.log(`Received ${event}:`, data);
// Handle event
if (event === 'subscriber.created') {
await handleNewSubscriber(data.subscriber);
}
// Respond quickly (< 5 seconds)
res.status(200).json({ received: true });
});
app.listen(3000);Step 2: Configure Webhook URL
- Log in to your dashboard
- Navigate to Project Settings
- Click "Webhooks" tab
- Enter your webhook URL (e.g.,
https://yoursite.com/webhooks/waitlist) - Select events you want to receive
- Copy the webhook secret (for signature verification)
- Click "Save"
Step 3: Test Webhook
Click "Send Test Event" to verify your endpoint receives events correctly.
Test Payload:
{
"event": "test",
"data": {
"message": "This is a test webhook"
},
"timestamp": "2025-10-22T14:00:00.000Z"
}Payload Structure
All webhook payloads follow this structure:
{
event: string; // Event type (e.g., 'subscriber.created')
data: object; // Event-specific data
timestamp: string; // ISO 8601 timestamp
projectId: string; // Project ID
webhookId: string; // Webhook delivery ID (for idempotency)
}subscriber.created
Triggered when a new subscriber joins your waitlist.
{
"event": "subscriber.created",
"data": {
"subscriber": {
"id": "sub_abc123",
"email": "user@example.com",
"subscribedAt": "2025-10-22T14:05:30.000Z",
"metadata": {
"source": "landing-page",
"referrer": "https://google.com",
"userAgent": "Mozilla/5.0..."
}
},
"project": {
"id": "proj_xyz789",
"name": "My Product",
"slug": "my-product"
}
},
"timestamp": "2025-10-22T14:05:31.000Z",
"projectId": "proj_xyz789",
"webhookId": "wh_delivery_abc123"
}Use Cases:
- Send welcome email
- Add to CRM (Salesforce, HubSpot)
- Update spreadsheet (Google Sheets, Airtable)
- Trigger Slack notification
- Track analytics event
subscriber.deleted
Triggered when a subscriber is removed from the waitlist.
{
"event": "subscriber.deleted",
"data": {
"subscriber": {
"id": "sub_abc123",
"email": "user@example.com"
},
"project": {
"id": "proj_xyz789",
"name": "My Product"
}
},
"timestamp": "2025-10-22T14:10:00.000Z",
"projectId": "proj_xyz789",
"webhookId": "wh_delivery_def456"
}Use Cases:
- Remove from email list
- Update CRM status
- Log for analytics
project.created (Pro Plan)
Triggered when a new project is created.
{
"event": "project.created",
"data": {
"project": {
"id": "proj_new123",
"name": "New Product Launch",
"slug": "new-product",
"createdAt": "2025-10-22T14:15:00.000Z"
}
},
"timestamp": "2025-10-22T14:15:01.000Z",
"projectId": "proj_new123",
"webhookId": "wh_delivery_ghi789"
}project.updated (Pro Plan)
Triggered when project settings are updated.
{
"event": "project.updated",
"data": {
"project": {
"id": "proj_xyz789",
"name": "Updated Product Name",
"slug": "my-product",
"updatedAt": "2025-10-22T14:20:00.000Z"
},
"changes": {
"name": {
"old": "My Product",
"new": "Updated Product Name"
}
}
},
"timestamp": "2025-10-22T14:20:01.000Z",
"projectId": "proj_xyz789",
"webhookId": "wh_delivery_jkl012"
}project.deleted (Pro Plan)
Triggered when a project is deleted.
{
"event": "project.deleted",
"data": {
"project": {
"id": "proj_xyz789",
"name": "My Product",
"slug": "my-product"
}
},
"timestamp": "2025-10-22T14:25:00.000Z",
"projectId": "proj_xyz789",
"webhookId": "wh_delivery_mno345"
}Signature Verification
🔒 CRITICAL: Always verify webhook signatures to ensure requests are from Waitlist Widget.
How It Works
- Waitlist Widget generates HMAC-SHA256 signature using your webhook secret
- Signature is sent in
X-Webhook-Signatureheader - Your server verifies the signature before processing the payload
Verification Code
Node.js
const crypto = require('crypto');
function verifySignature(payload, signature, secret) {
// payload: stringified JSON body
// signature: X-Webhook-Signature header value
// secret: your webhook secret from dashboard
const expectedSignature = crypto
.createHmac('sha256', secret)
.update(payload)
.digest('hex');
// Constant-time comparison to prevent timing attacks
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(`sha256=${expectedSignature}`)
);
}
// Usage in Express
app.post('/webhooks/waitlist', (req, res) => {
const signature = req.headers['x-webhook-signature'];
const payload = JSON.stringify(req.body);
if (!verifySignature(payload, signature, process.env.WEBHOOK_SECRET)) {
return res.status(401).json({ error: 'Invalid signature' });
}
// Process event
res.status(200).json({ received: true });
});Python
import hmac
import hashlib
def verify_signature(payload: str, signature: str, secret: str) -> bool:
"""
Verify webhook signature.
Args:
payload: JSON string of request body
signature: X-Webhook-Signature header value
secret: Webhook secret from dashboard
Returns:
True if signature is valid, False otherwise
"""
expected_signature = hmac.new(
secret.encode('utf-8'),
payload.encode('utf-8'),
hashlib.sha256
).hexdigest()
expected_header = f'sha256={expected_signature}'
# Constant-time comparison
return hmac.compare_digest(signature, expected_header)
# Usage in Flask
from flask import Flask, request, jsonify
import json
app = Flask(__name__)
@app.route('/webhooks/waitlist', methods=['POST'])
def webhook():
signature = request.headers.get('X-Webhook-Signature')
payload = json.dumps(request.get_json(), separators=(',', ':'))
if not verify_signature(payload, signature, os.environ['WEBHOOK_SECRET']):
return jsonify({'error': 'Invalid signature'}), 401
# Process event
event = request.get_json()
print(f"Received {event['event']}: {event['data']}")
return jsonify({'received': True}), 200PHP
<?php
function verifySignature($payload, $signature, $secret) {
$expectedSignature = hash_hmac('sha256', $payload, $secret);
$expectedHeader = "sha256={$expectedSignature}";
// Constant-time comparison
return hash_equals($signature, $expectedHeader);
}
// Usage
$payload = file_get_contents('php://input');
$signature = $_SERVER['HTTP_X_WEBHOOK_SIGNATURE'];
$secret = getenv('WEBHOOK_SECRET');
if (!verifySignature($payload, $signature, $secret)) {
http_response_code(401);
echo json_encode(['error' => 'Invalid signature']);
exit;
}
$event = json_decode($payload, true);
// Process event
http_response_code(200);
echo json_encode(['received' => true]);
?>Retry Logic
If your endpoint fails to respond or returns an error, Waitlist Widget automatically retries delivery.
Retry Schedule
| Attempt | Delay | Total Time |
|---|---|---|
| 1 | Immediate | 0s |
| 2 | 5 seconds | 5s |
| 3 | 30 seconds | 35s |
| 4 | 5 minutes | 5m 35s |
| 5 | 30 minutes | 35m 35s |
| 6 | 2 hours | 2h 35m 35s |
| 7 | 12 hours | 14h 35m 35s |
After 7 failed attempts, the webhook is marked as failed and retries stop.
Successful Response
Return 200 OK within 5 seconds:
res.status(200).json({ received: true });Failed Response
Any of these are considered failures:
- Non-2xx status codes (400, 500, etc.)
- Timeout (> 5 seconds)
- Network errors
- SSL certificate errors
Idempotency
Use webhookId to prevent duplicate processing:
const processedWebhooks = new Set(); // or database
app.post('/webhooks/waitlist', async (req, res) => {
const { webhookId, event, data } = req.body;
// Check if already processed
if (processedWebhooks.has(webhookId)) {
return res.status(200).json({ received: true, message: 'Already processed' });
}
// Process event
await handleEvent(event, data);
// Mark as processed
processedWebhooks.add(webhookId);
res.status(200).json({ received: true });
});Integration Examples
Zapier Integration
No Code Required!
- Create Zap: Webhooks by Zapier → Action
- Get Webhook URL: Copy the URL
- Configure in Dashboard: Paste URL in Waitlist Widget webhook settings
- Test: Send test event
- Add Actions: Gmail, Slack, Airtable, etc.
Example Zap Flow:
Waitlist Widget (subscriber.created)
→ Filter: Only new emails
→ Gmail: Send welcome email
→ Google Sheets: Add row
→ Slack: Post to #new-signupsMake (Integromat) Integration
- Create Scenario: Webhooks → Custom webhook
- Copy Webhook URL
- Configure in Dashboard
- Add Modules: CRM, Email, Database, etc.
n8n Integration (Self-Hosted)
{
"nodes": [
{
"name": "Webhook",
"type": "n8n-nodes-base.webhook",
"parameters": {
"path": "waitlist",
"responseMode": "onReceived",
"responseData": "allEntries"
}
},
{
"name": "Filter",
"type": "n8n-nodes-base.filter",
"parameters": {
"conditions": {
"string": [
{
"value1": "={{$json[\"event\"]}}",
"value2": "subscriber.created"
}
]
}
}
},
{
"name": "Send Email",
"type": "n8n-nodes-base.emailSend",
"parameters": {
"to": "={{$json[\"data\"][\"subscriber\"][\"email\"]}}",
"subject": "Welcome to the waitlist!",
"text": "Thanks for joining!"
}
}
]
}Custom Integration (Next.js API Route)
// app/api/webhooks/waitlist/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { verifySignature } from '@/lib/webhook-verification';
import { prisma } from '@/lib/prisma';
import { sendWelcomeEmail } from '@/lib/email';
export async function POST(request: NextRequest) {
try {
// Get signature
const signature = request.headers.get('x-webhook-signature');
if (!signature) {
return NextResponse.json(
{ error: 'Missing signature' },
{ status: 401 }
);
}
// Get payload
const payload = await request.text();
const event = JSON.parse(payload);
// Verify signature
const isValid = verifySignature(
payload,
signature,
process.env.WEBHOOK_SECRET!
);
if (!isValid) {
return NextResponse.json(
{ error: 'Invalid signature' },
{ status: 401 }
);
}
// Check idempotency
const existingWebhook = await prisma.webhookLog.findUnique({
where: { webhookId: event.webhookId }
});
if (existingWebhook) {
return NextResponse.json({ received: true, message: 'Already processed' });
}
// Process event
if (event.event === 'subscriber.created') {
const { subscriber } = event.data;
// Add to database
await prisma.subscriber.create({
data: {
email: subscriber.email,
source: 'waitlist',
waitlistId: subscriber.id
}
});
// Send welcome email
await sendWelcomeEmail(subscriber.email);
// Log webhook
await prisma.webhookLog.create({
data: {
webhookId: event.webhookId,
event: event.event,
processedAt: new Date()
}
});
}
return NextResponse.json({ received: true });
} catch (error) {
console.error('Webhook error:', error);
return NextResponse.json(
{ error: 'Internal server error' },
{ status: 500 }
);
}
}Troubleshooting
Webhooks not being delivered?
Check these:
- Endpoint accessibility: Must be publicly accessible (not localhost)
- HTTPS required: Production webhooks require HTTPS with valid SSL certificate
- Response time: Endpoint must respond within 5 seconds
- Status code: Must return 2xx status
- Firewall/VPN: Ensure Waitlist Widget IPs aren't blocked
Signature verification failing?
Common issues:
- Payload modification: Don't modify JSON before verification
- Character encoding: Use UTF-8 encoding
- JSON serialization: Preserve original formatting (no extra spaces)
- Secret mismatch: Copy-paste secret carefully from dashboard
- Header name: Check
X-Webhook-Signature(case-sensitive)
Debugging Tips
// Log everything for debugging
app.post('/webhooks/waitlist', (req, res) => {
console.log('Headers:', req.headers);
console.log('Body:', req.body);
console.log('Signature:', req.headers['x-webhook-signature']);
// Verify signature
const payload = JSON.stringify(req.body);
const signature = req.headers['x-webhook-signature'];
const secret = process.env.WEBHOOK_SECRET;
console.log('Payload for verification:', payload);
console.log('Secret (first 10 chars):', secret.substring(0, 10) + '...');
const isValid = verifySignature(payload, signature, secret);
console.log('Signature valid:', isValid);
res.status(200).json({ received: true, valid: isValid });
});Testing Locally
Use ngrok to expose localhost:
# Install ngrok
npm install -g ngrok
# Expose port 3000
ngrok http 3000
# Copy ngrok HTTPS URL (e.g., https://abc123.ngrok.io)
# Paste into webhook settings: https://abc123.ngrok.io/webhooks/waitlistBest Practices
1. Security
- ✅ Always verify signatures
- ✅ Use HTTPS in production
- ✅ Rotate webhook secrets periodically
- ✅ Rate limit webhook endpoint
- ✅ Log failed attempts for monitoring
2. Reliability
- ✅ Respond quickly (< 5 seconds)
- ✅ Process asynchronously (queue jobs)
- ✅ Implement idempotency (check
webhookId) - ✅ Handle retries gracefully
- ✅ Monitor webhook failures
3. Scalability
// ✅ Good: Queue for async processing
app.post('/webhooks/waitlist', async (req, res) => {
// Verify signature
if (!verifySignature(...)) {
return res.status(401).json({ error: 'Invalid signature' });
}
// Queue job (Redis, Bull, AWS SQS, etc.)
await queue.add('process-webhook', req.body);
// Respond immediately
res.status(200).json({ received: true });
});
// ❌ Bad: Synchronous slow processing
app.post('/webhooks/waitlist', async (req, res) => {
// This takes > 5 seconds → timeout!
await sendEmail(...);
await updateDatabase(...);
await callExternalAPI(...);
res.status(200).json({ received: true }); // Too late!
});Webhook Dashboard
Monitor webhook deliveries in your dashboard:
- Recent Deliveries: Last 100 webhook events
- Success Rate: Percentage of successful deliveries
- Failed Deliveries: View error details
- Retry Manually: Resend failed webhooks
API Reference
See full webhook API documentation: API.md
Support
Questions? We're here to help!
- 📧 Email: support@waitlistwidget.com - We typically respond within 24 hours
- 📚 Documentation: See QUICKSTART.md, API.md, and FAQ.md
Feedback? We'd love to hear from you at support@waitlistwidget.com