Webhooks Integration

Webhooks Integration Guide

Automate your workflows with real-time webhook notifications. 🔔

Table of Contents


Overview

Webhooks allow your server to receive real-time notifications when events occur in your Waitlist Widget account.

Supported Events

EventTriggerAvailable Plans
subscriber.createdNew subscriber joins waitlistStarter, Pro
subscriber.deletedSubscriber removedStarter, Pro
project.createdNew project createdPro
project.updatedProject settings updatedPro
project.deletedProject deletedPro

⚠️ 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

  1. Log in to your dashboard
  2. Navigate to Project Settings
  3. Click "Webhooks" tab
  4. Enter your webhook URL (e.g., https://yoursite.com/webhooks/waitlist)
  5. Select events you want to receive
  6. Copy the webhook secret (for signature verification)
  7. 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

  1. Waitlist Widget generates HMAC-SHA256 signature using your webhook secret
  2. Signature is sent in X-Webhook-Signature header
  3. 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}), 200

PHP

<?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

AttemptDelayTotal Time
1Immediate0s
25 seconds5s
330 seconds35s
45 minutes5m 35s
530 minutes35m 35s
62 hours2h 35m 35s
712 hours14h 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!

  1. Create Zap: Webhooks by Zapier → Action
  2. Get Webhook URL: Copy the URL
  3. Configure in Dashboard: Paste URL in Waitlist Widget webhook settings
  4. Test: Send test event
  5. 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-signups

Make (Integromat) Integration

  1. Create Scenario: Webhooks → Custom webhook
  2. Copy Webhook URL
  3. Configure in Dashboard
  4. 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:

  1. Endpoint accessibility: Must be publicly accessible (not localhost)
  2. HTTPS required: Production webhooks require HTTPS with valid SSL certificate
  3. Response time: Endpoint must respond within 5 seconds
  4. Status code: Must return 2xx status
  5. Firewall/VPN: Ensure Waitlist Widget IPs aren't blocked

Signature verification failing?

Common issues:

  1. Payload modification: Don't modify JSON before verification
  2. Character encoding: Use UTF-8 encoding
  3. JSON serialization: Preserve original formatting (no extra spaces)
  4. Secret mismatch: Copy-paste secret carefully from dashboard
  5. 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/waitlist

Best 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!

Feedback? We'd love to hear from you at support@waitlistwidget.com