Help & Docs Webhooks

Webhooks

Get an HTTP POST to your server the moment a subscriber joins. No polling, no delays — wire up any automation in minutes.

Delivery: HTTP POST Signed: X-PLW-Signature Format: application/json

Overview

Webhooks let you react to waitlist events in real time. When someone signs up, PreLaunch Waitlist fires an HTTP POST request to every URL you have registered for that project.

Common use cases include triggering a welcome email through SendGrid, pushing a record into your CRM, updating a spreadsheet via Zapier or Make, or running custom onboarding logic on your own server.

You can also set up Slack notifications from the dashboard — no webhook server needed.

Registering a webhook

Register webhooks from the Webhooks tab in your project dashboard.

  1. Open your project in the dashboard.
  2. Navigate to Webhooks.
  3. Click + Add webhook and paste your endpoint URL.
  4. Copy the signing secret shown — it is only displayed once.

API key management is coming soon — register webhooks from the dashboard for now.

Events

Event When it fires
subscriber.created A new, unique email address is added to the waitlist. Duplicate sign-ups do not fire this event.

Additional events (e.g. subscriber.deleted) will be added in a future release.

Payload structure

Every delivery sends a JSON body with a consistent envelope. The event field tells you which event fired; the data object contains the event-specific details.

subscriber.created payload
{
  "event": "subscriber.created",
  "projectId": "abc123",
  "timestamp": "2026-04-11T14:32:07.000Z",
  "data": {
    "email": "[email protected]",
    "subscribedAt": "2026-04-11T14:32:06.000Z",
    "metadata": {}
  }
}

Top-level fields

FieldTypeDescription
event string The event type that triggered the delivery.
projectId string The ID of the project this subscriber joined.
timestamp ISO 8601 string UTC time the delivery was dispatched.
data object Event-specific data (see below).

data fields — subscriber.created

FieldTypeDescription
email string The normalised (lowercase, trimmed) subscriber email.
subscribedAt ISO 8601 string | null When the subscriber document was written to the database.
metadata object Any extra data passed by the SDK at sign-up time (e.g. UTM parameters or custom form fields).

Signature verification

Every delivery includes an X-PLW-Signature header containing an HMAC-SHA256 digest of the raw request body, computed using the secret you received when you registered the webhook.

Header format
X-PLW-Signature: sha256=a3f8c1d2e5f047b9ad2e...

To verify a delivery, compute an HMAC-SHA256 of the raw request body using your secret, then compare it to the value after the sha256= prefix. Always use a constant-time comparison to prevent timing attacks.

Always verify the signature before acting on a webhook payload. Without verification, anyone who knows your endpoint URL could send fraudulent events.

Integration examples

The examples below use Express (Node.js), Flask (Python), and plain PHP. Each verifies the HMAC signature before processing the payload.

Node.js / Express
import express from 'express';
import crypto from 'crypto';

const app = express();
const PLW_SECRET = process.env.PLW_WEBHOOK_SECRET; // your signing secret

// Parse raw body for signature verification
app.post(
  '/webhooks/plw',
  express.raw({ type: 'application/json' }),
  (req, res) => {
    // 1. Verify signature
    const signature = req.headers['x-plw-signature'];
    const expected  = 'sha256=' + crypto
      .createHmac('sha256', PLW_SECRET)
      .update(req.body)      // req.body is a Buffer here
      .digest('hex');

    if (!crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))) {
      return res.status(401).send('Invalid signature');
    }

    // 2. Parse and handle the event
    const event = JSON.parse(req.body.toString());

    if (event.event === 'subscriber.created') {
      const { email, subscribedAt } = event.data;
      console.log(`New subscriber: ${email} at ${subscribedAt}`);
      // → trigger welcome email, update CRM, etc.
    }

    res.status(200).send('OK');
  }
);

app.listen(3000);
Python / Flask
import hashlib
import hmac
import json
import os
from flask import Flask, request, abort

app = Flask(__name__)
PLW_SECRET = os.environ['PLW_WEBHOOK_SECRET'].encode()

@app.route('/webhooks/plw', methods=['POST'])
def plw_webhook():
    # 1. Verify signature
    signature  = request.headers.get('X-PLW-Signature', '')
    body_bytes = request.get_data()
    digest     = 'sha256=' + hmac.new(PLW_SECRET, body_bytes, hashlib.sha256).hexdigest()

    if not hmac.compare_digest(signature, digest):
        abort(401)

    # 2. Parse and handle the event
    event = json.loads(body_bytes)

    if event['event'] == 'subscriber.created':
        email = event['data']['email']
        print(f'New subscriber: {email}')
        # → trigger welcome email, update CRM, etc.

    return 'OK', 200
PHP
<?php
$secret = getenv('PLW_WEBHOOK_SECRET');
$body   = file_get_contents('php://input');

// 1. Verify signature
$signature = $_SERVER['HTTP_X_PLW_SIGNATURE'] ?? '';
$expected  = 'sha256=' . hash_hmac('sha256', $body, $secret);

if (!hash_equals($expected, $signature)) {
    http_response_code(401);
    exit('Invalid signature');
}

// 2. Parse and handle the event
$event = json_decode($body, true);

if ($event['event'] === 'subscriber.created') {
    $email = $event['data']['email'];
    error_log("New subscriber: $email");
    // → trigger welcome email, update CRM, etc.
}

http_response_code(200);
echo 'OK';

Request headers

Every webhook delivery includes the following HTTP headers.

HeaderValueDescription
Content-Type application/json The body is always JSON-encoded UTF-8.
X-PLW-Signature sha256=<hex> HMAC-SHA256 of the raw request body using your signing secret.
X-PLW-Event e.g. subscriber.created The event type that triggered this delivery.

Retries & timeouts

Each delivery attempt has a 10-second timeout. If your endpoint does not respond within that window, or responds with a non-2xx status, the delivery is logged as failed but not retried automatically.

To avoid missed deliveries, keep your handler fast. Acknowledge the request with a 200 status immediately and process the payload asynchronously (e.g. push to a queue). Avoid long database writes or third-party API calls inside the synchronous handler.

Retry logic is on the roadmap. Until then, if reliability is critical, consider using a managed webhook relay like Svix or Hookdeck between your endpoint and the PreLaunch Waitlist delivery.

Testing locally

Use a tunnelling tool to expose your local server to the internet while developing. Popular options:

ToolQuick start
ngrok ngrok http 3000
localtunnel npx localtunnel --port 3000
Hookdeck CLI hookdeck listen 3000 plw-source

Once your tunnel is running, register the public URL as a webhook in your project dashboard. Trigger a test sign-up and you should see the delivery arrive in your local handler.