Waitlist guides
06 · Webhooks + analytics

Hear about new signups. Measure the funnel.

Webhooks push entry events into your stack. Analytics answers how many, how fast, and who is bringing friends.


1

The three events

Webhooks belong to the workspace: an endpoint receives events from every waitlist in it, and each event says which waitlist it came from.

  • entry.created — someone joined a waitlist.
  • entry.approved — an entry was approved, by you or automatically.
  • entry.rejected — an entry was rejected.

On a waitlist that does not need approval, one signup sends both entry.created and entry.approved, so your code has one path for approved people. The list, with a description and an example payload for each event, is also served by the API:

GET /v1/workspaces/:workspace_id/webhooks/event-types
curl https://api.waitlist.productcraft.co/v1/workspaces/<workspace_id>/webhooks/event-types \
  -H 'authorization: Bearer pcft_live_...'

2

Add an endpoint

From the console (Waitlist → Webhooks) or the API. Endpoints, signatures, tests, retries and replay work the same in every ProductCraft product.

POST /v1/workspaces/:workspace_id/webhooks/endpoints
curl -X POST https://api.waitlist.productcraft.co/v1/workspaces/<workspace_id>/webhooks/endpoints \
  -H 'authorization: Bearer pcft_live_...' \
  -H 'idempotency-key: add-signups-endpoint-1' \
  -H 'content-type: application/json' \
  -d '{
    "url":         "https://acme.example.com/webhooks/waitlist",
    "event_types": ["entry.created", "entry.approved"]
  }'

The answer carries the whsec_… signing secret exactly once. Up to 10 endpoints per workspace. Verifying the signature, Send test, automatic retries, auto-disable, the event log, replay and secret rotation are in the Webhooks guide, with Node code you can paste.


3

What an entry event looks like

Signed with Standard Webhooks like every ProductCraft event; the entry is in data.entry and the waitlist in waitlist_id.

entry.created
{
  "id":           "evt_83cfc794-78e9-4102-96de-3a9a6f5bc31e",
  "type":         "entry.created",
  "created_at":   "2026-10-17T10:00:00.000Z",
  "workspace_id": "24fab978-7cd7-48ae-bce1-aba876b11957",
  "waitlist_id":  "33a8b309-8753-4f08-90d7-9dc484eff392",
  "data": {
    "waitlist_id": "33a8b309-8753-4f08-90d7-9dc484eff392",
    "entry": {
      "id":                   "1d0b7828-0e1a-407b-8706-aeb7ff619088",
      "email":                "ada@example.com",
      "name":                 "Ada Lovelace",
      "position":             4,
      "status":               "pending",
      "referral_code":        "464QTHE0",
      "referral_count":       0,
      "referred_by_entry_id": null,
      "created_at":           "2026-10-17T10:00:00.000Z",
      "updated_at":           "2026-10-17T10:00:00.000Z"
    }
  }
}

Custom field values (metadata), interest and referrer are not in the payload; fetch the entry by id if you need them.

Set up before 17 October 2026? Endpoints that existed then, or that are created through the older POST .../webhooks route, also get the old x-waitlist-signature header plus x-waitlist-event and x-waitlist-event-id, and the body still carries the old top-level event, occurred_at and entry fields, until 15 January 2027. Read type and data.entry before then. x-waitlist-delivery-attempt is no longer sent; dedupe on webhook-id. The Webhooks guide covers the old header and moving your verifier.


4

Analytics

GET /v1/workspaces/:workspace_id/waitlists/:waitlist_id/analytics
curl https://api.waitlist.productcraft.co/v1/workspaces/<workspace_id>/waitlists/<waitlist_id>/analytics \
  -H 'authorization: Bearer pcft_live_...'
{
  "waitlist_id":       "33a8b309-8753-4f08-90d7-9dc484eff392",
  "total_entries":     7,
  "by_status":         { "pending": 0, "approved": 7, "rejected": 0 },
  "signups_today":     7,
  "signups_this_week": 7,
  "signups_this_month": 7,
  "referred_count":    3,
  "viral_coefficient": 0.4285714285714286,
  "top_referrers": [
    { "referrer": null, "count": 7 }
  ],
  "top_referrals": [
    {
      "id":            "1e1e0c99-26a2-4f07-982c-eb60d20305f6",
      "email":         "ada@example.com",
      "name":          "Ada Lovelace",
      "referral_code": "SCDQ0TZB",
      "referral_count": 2,
      "position":       5,
      "effective_position": 1
    }
  ]
}
  • viral_coefficient — referred_count / total_entries, the K-factor proxy. 0 when the waitlist is empty.
  • top_referrers groups the free-text referrer field (“how did you hear about us”), not referral codes. Entries that left it blank collapse into the null bucket.
  • top_referrals is the code-based leaderboard — full PII, ordered by referral_count, with the effective_position from chapter 5.

There is no impressions or conversion metric: Waitlist never sees your landing page, only the submits.


5

Timeline

Daily signup counts, UTC, dense — every day in the window gets a row even when it is zero.

GET /v1/workspaces/:workspace_id/waitlists/:waitlist_id/analytics/timeline
curl 'https://api.waitlist.productcraft.co/v1/workspaces/<workspace_id>/waitlists/<waitlist_id>/analytics/timeline?since=2026-07-26&until=2026-07-29' \
  -H 'authorization: Bearer pcft_live_...'
{
  "granularity": "day",
  "points": [
    { "date": "2026-07-26", "count": 0 },
    { "date": "2026-07-27", "count": 0 },
    { "date": "2026-07-28", "count": 3 }
  ]
}

since (inclusive) and until (exclusive) are both required ISO-8601 values — omitting either is a 400. The window must be forward-going and at most 366 days. Granularity is always daily; there is no bucket parameter.


6

CSV export

GET /v1/workspaces/:workspace_id/waitlists/:waitlist_id/entries/export.csv
curl -o entries.csv \
  https://api.waitlist.productcraft.co/v1/workspaces/<workspace_id>/waitlists/<waitlist_id>/entries/export.csv \
  -H 'authorization: Bearer pcft_live_...'
entries.csv
id,email,name,position,status,interest,referrer,referral_code,referral_count,referred_by_entry_id,invited_at,invited_to_app_slug,created_at
7c2b32e2-...,ada@example.com,Ada Lovelace,1,approved,,,DYPG2KK3,1,,,,2026-07-28T20:16:51.346Z

Streams every entry on the waitlist — the endpoint takes no filters, so slice it downstream. Custom field values are not columns; use GET .../entries and read metadata when you need them.


What's next

You've finished the series