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.
- Open your project in the dashboard.
- Navigate to Webhooks.
- Click + Add webhook and paste your endpoint URL.
- 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.
{
"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
| Field | Type | Description |
|---|---|---|
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
| Field | Type | Description |
|---|---|---|
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.
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.
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);
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
$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.
| Header | Value | Description |
|---|---|---|
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:
| Tool | Quick 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.