Waitlist docs
API reference

Waitlist API.

Public signup endpoints anyone can hit from a frontend, plus the admin endpoints that drive the console. Every endpoint generated from the live OpenAPI spec.

Waitlist exposes two surfaces. The public surface under /v1/waitlists/{workspace_slug}/{waitlist_slug} needs no auth: your own frontend reads the waitlist config with GET and posts signups to POST .../entries. There is no hosted signup page — you render the form. The admin surface under /v1/workspaces/{workspace_id}/... needs a bearer token (platform session or pcft_live_* key) and covers waitlists, entries, variants, analytics, and webhooks. Both are documented below.

Base URL

https://api.waitlist.productcraft.co

Auth

Authorization: Bearer …

Spec version

OpenAPI 3.0.0 · v0.1.0

Waitlist-Settings

get/v1/workspaces/{workspace_id}/waitlist/settingsAuth

Read workspace-level Waitlist defaults

The settings plus `email_setup` (whether Waitlist emails can go out and what is missing; the sender domain and template names are checked against Mail, cached for up to 60 seconds) and `skipped_sends` (emails not sent in the last 7 days because the setup was incomplete). Waitlist never falls back to a ProductCraft sender: an email that cannot go out is recorded, not sent.

Path parameters

workspace_id*string

Response · 200 Workspace Waitlist settings with the email setup state.

workspace_id*string · uuid
default_sender_domain_id*string · uuid

Mail domain UUID used as default sender for Waitlist notifications.

default_heimdall_app_slug*string

Auth consumer-app slug approved entries are invited into.

brand_name*string
brand_logo_url*string
primary_color*string

Primary brand colour as #RRGGBB

notifications_via_envoi*boolean

Whether Waitlist notifications are routed through the Mail service.

sender_mode*enum (2)

Where the From comes from: `workspace` (the workspace default sender set in Mail; the default) or `custom` (`default_sender_domain_id` + `default_sender_address`). Workspaces that had an address before this setting existed are `custom`.

Example: "workspace"

effective_sender*object

The sender Waitlist emails actually go out from, with its live status in Mail.

default_sender_address*string

The From used when `sender_mode` is `custom`.

welcome_template_name*string
approved_template_name*string
rejected_template_name*string
updated_at*string · date-time

null when the workspace has no explicit settings row yet.

email_setup*object

Whether Waitlist emails can go out and, if not, what is missing. Computed on the server on every read.

skipped_sends*object

Emails Waitlist did not send in the last 7 days because the setup was incomplete.

Example

Request

GET /v1/workspaces/{workspace_id}/waitlist/settings
Authorization: Bearer YOUR_TOKEN

Response

{
  "workspace_id": "00000000-0000-0000-0000-000000000000",
  "default_sender_domain_id": "00000000-0000-0000-0000-000000000000",
  "default_heimdall_app_slug": "string",
  "brand_name": "string",
  "brand_logo_url": "string",
  "primary_color": "string",
  "notifications_via_envoi": false,
  "sender_mode": "workspace",
  "effective_sender": {},
  "default_sender_address": "string",
  "welcome_template_name": "string",
  "approved_template_name": "string",
  "rejected_template_name": "string",
  "updated_at": "2026-01-01T00:00:00.000Z",
  "email_setup": {},
  "skipped_sends": {}
}
put/v1/workspaces/{workspace_id}/waitlist/settingsAuth

Update workspace-level Waitlist defaults

Path parameters

workspace_id*string

Request body

default_sender_domain_idstring

Mail domain UUID to use as default sender for Waitlist notifications. null clears the override.

default_heimdall_app_slugstring

Auth consumer-app slug to invite approved entries into. null clears the link.

brand_namestring

Brand name returned in the public waitlist payload. Customer frontends render it on their own signup form.

brand_logo_urlstring

Logo URL returned in the public waitlist payload. Customer frontends render it on their own signup form.

primary_colorstring

Primary brand colour as #RRGGBB

notifications_via_envoiboolean

Route Waitlist notifications through the Mail service instead of staying silent. Needs a sender (the workspace default in `workspace` mode, or the custom domain + address) and a template for every event. Turning it on (from off) creates the default templates `waitlist-welcome`, `waitlist-approved` and `waitlist-rejected` in Mail for every empty slot, reusing a template that already has that name, with your own Mail access. Default false; when off, Waitlist sends no notification emails and the customer is expected to handle email via the webhook lane.

sender_modeenum (2)

Where the From comes from. `workspace` (the default): the workspace default sender set in Mail, resolved at send time, so changing it in Mail changes it for every product. `custom`: `default_sender_domain_id` + `default_sender_address`. Sending `default_sender_address` without `sender_mode` sets `custom`; sending it as null sets `workspace`.

Example: "workspace"

default_sender_addressstring · email

The From used when `sender_mode` is `custom` (kept, but unused, in `workspace` mode). Must belong to default_sender_domain_id. null clears it.

welcome_template_namestring

Mail template name used for entry.created notifications. Must already exist in the workspace template store.

approved_template_namestring

Mail template name used for entry.approved notifications. Must already exist in the workspace template store.

rejected_template_namestring

Mail template name used for entry.rejected notifications. Must already exist in the workspace template store.

Response · 200 Updated workspace Waitlist settings with the email setup state.

workspace_id*string · uuid
default_sender_domain_id*string · uuid

Mail domain UUID used as default sender for Waitlist notifications.

default_heimdall_app_slug*string

Auth consumer-app slug approved entries are invited into.

brand_name*string
brand_logo_url*string
primary_color*string

Primary brand colour as #RRGGBB

notifications_via_envoi*boolean

Whether Waitlist notifications are routed through the Mail service.

sender_mode*enum (2)

Where the From comes from: `workspace` (the workspace default sender set in Mail; the default) or `custom` (`default_sender_domain_id` + `default_sender_address`). Workspaces that had an address before this setting existed are `custom`.

Example: "workspace"

effective_sender*object

The sender Waitlist emails actually go out from, with its live status in Mail.

default_sender_address*string

The From used when `sender_mode` is `custom`.

welcome_template_name*string
approved_template_name*string
rejected_template_name*string
updated_at*string · date-time

null when the workspace has no explicit settings row yet.

email_setup*object

Whether Waitlist emails can go out and, if not, what is missing. Computed on the server on every read.

skipped_sends*object

Emails Waitlist did not send in the last 7 days because the setup was incomplete.

Example

Request

PUT /v1/workspaces/{workspace_id}/waitlist/settings
Authorization: Bearer YOUR_TOKEN
Content-Type: application/json

{
  "default_sender_domain_id": "string",
  "default_heimdall_app_slug": "string",
  "brand_name": "string",
  "brand_logo_url": "string",
  "primary_color": "string",
  "notifications_via_envoi": false,
  "sender_mode": "workspace",
  "default_sender_address": "user@example.com",
  "welcome_template_name": "string",
  "approved_template_name": "string",
  "rejected_template_name": "string"
}

Response

{
  "workspace_id": "00000000-0000-0000-0000-000000000000",
  "default_sender_domain_id": "00000000-0000-0000-0000-000000000000",
  "default_heimdall_app_slug": "string",
  "brand_name": "string",
  "brand_logo_url": "string",
  "primary_color": "string",
  "notifications_via_envoi": false,
  "sender_mode": "workspace",
  "effective_sender": {},
  "default_sender_address": "string",
  "welcome_template_name": "string",
  "approved_template_name": "string",
  "rejected_template_name": "string",
  "updated_at": "2026-01-01T00:00:00.000Z",
  "email_setup": {},
  "skipped_sends": {}
}
post/v1/workspaces/{workspace_id}/waitlist/settings/notifications/{slot}/testAuth

Send me a test of one Waitlist email

Sends the slot's real Mail template, with the workspace's real Waitlist sender (the workspace default or the custom address) and example values, to your own verified account address. It cannot be sent anywhere else. It uses your own Mail access, goes through the same send path as real emails, and shows up in Mail > Emails. At most 10 tests per person per hour. Not available to API keys (a key has no inbox).

Path parameters

workspace_id*string
slot*enum (3)

The email to test.

Headers

Idempotency-Keystring

Stripe-style idempotency key. A retried request with the same key returns the first response instead of sending a second email. 24h TTL.

Response · 202 Accepted by Mail and queued for delivery.

to*string · email

Your own verified account address: tests only ever go to the person who asked.

Example: "you@acme.com"

template_name*string

The Mail template that was sent.

Example: "waitlist-welcome"

from*string

The From it went out with.

Example: "Acme <hello@acme.com>"

message_id*string · uuid

The Mail message id; look it up in Mail > Emails to follow delivery.

Example: "0d6c1c3e-5f2a-4b8e-9d17-3a4b5c6d7e8f"

Example

Request

POST /v1/workspaces/{workspace_id}/waitlist/settings/notifications/{slot}/test
Authorization: Bearer YOUR_TOKEN

Response

{
  "to": "you@acme.com",
  "template_name": "waitlist-welcome",
  "from": "Acme <hello@acme.com>",
  "message_id": "0d6c1c3e-5f2a-4b8e-9d17-3a4b5c6d7e8f"
}

Waitlists

get/v1/workspaces/{workspace_id}/waitlistsAuth

List waitlists in this workspace

Path parameters

workspace_id*string · uuid

Query parameters

limitnumber
cursorstring

Opaque cursor for pagination

statusstring

Filter by status

Response · 200 Page of waitlists.

data*array
pagination*object
next_cursor*string
has_more*boolean

Example

Request

GET /v1/workspaces/{workspace_id}/waitlists
Authorization: Bearer YOUR_TOKEN

Response

{
  "data": [
    {
      "id": "b1a4...",
      "workspace_id": "b1a4...",
      "workspace_slug": "acme",
      "slug": "early-access",
      "display_name": "Early Access Program",
      "description": "string",
      "status": "draft",
      "accepting_signups": false,
      "signups_note": "This waitlist is a draft, so public sign-ups are refused (422) until you change its status to active.",
      "allowed_origins": [],
      "settings": {},
      "created_by": "string",
      "previous_workspace_slug": "string",
      "created_at": "string",
      "updated_at": "string"
    }
  ],
  "pagination": {
    "next_cursor": "string",
    "has_more": false
  }
}
post/v1/workspaces/{workspace_id}/waitlistsAuth

Create a waitlist (workspace-scoped)

Pass `status: "active"` to accept public sign-ups straight away. Without it the waitlist starts as a draft, and the response says so in `accepting_signups: false` and `signups_note`.

Path parameters

workspace_id*string · uuid

Headers

Idempotency-Keystring

Stripe-style idempotency key. Waitlist creation provisions a non-trivial sub-tree (variant, default referral settings, etc.); a retry without this header creates a second waitlist sharing the same name slug. 24h TTL.

Request body

slug*string

URL-safe slug

Example: "early-access"

display_name*string

Display name

Example: "Early Access Program"

descriptionstring

Description returned in the public waitlist payload — customer frontends typically render it under the display name on their signup form.

settingsobject

Waitlist settings (max_entries, approval_required, custom_fields, confirmation_message, referral_boost, referral_rewards, recaptcha_site_key). To turn on reCAPTCHA v3, send recaptcha_site_key together with recaptcha_secret_key (both from your reCAPTCHA admin console); the secret is stored encrypted and is never returned. Send recaptcha_site_key: null to turn it off.

statusenum (2)

Start state. `active` accepts public sign-ups straight away; `draft` (the default) refuses them with 422 until the status is changed to `active`.

allowed_originsarray

Web origins (scheme and host, for example `https://acme.com` or `http://localhost:3000`) that browsers may submit public sign-ups from. `https://*.acme.com` matches any subdomain. An empty list means any origin. A browser request from another origin gets 403 `ORIGIN_NOT_ALLOWED`; server-side calls without an `Origin` header are not checked. Entries are normalised (lower case, no trailing slash, default port dropped).

Example: ["https://acme.com","https://*.acme.com"]

Response · 201 Waitlist created.

id*string

Example: "b1a4..."

workspace_id*string

Example: "b1a4..."

workspace_slug*string

Example: "acme"

slug*string

Example: "early-access"

display_name*string

Example: "Early Access Program"

description*string
status*enum (4)
accepting_signups*boolean

Whether the public sign-up endpoint accepts entries right now (only an `active` waitlist does).

Example: false

signups_note*string

Plain explanation when public sign-ups are refused, and how to open them; null when the waitlist is active.

Example: "This waitlist is a draft, so public sign-ups are refused (422) until you change its status to active."

allowed_origins*array

Web origins (scheme and host, for example `https://acme.com` or `http://localhost:3000`) that browsers may submit public sign-ups from. `https://*.acme.com` matches any subdomain. An empty list means any origin. A browser request from another origin gets 403 `ORIGIN_NOT_ALLOWED`; server-side calls without an `Origin` header are not checked. Entries are normalised (lower case, no trailing slash, default port dropped).

Example: []

settings*object

Waitlist settings (max_entries, approval_required, custom_fields, confirmation_message, referral_boost, referral_rewards, recaptcha_site_key). The reCAPTCHA secret key is never returned; recaptcha_secret_configured says whether one is saved.

created_by*string
previous_workspace_slug*string
created_at*string
updated_at*string

Example

Request

POST /v1/workspaces/{workspace_id}/waitlists
Authorization: Bearer YOUR_TOKEN
Content-Type: application/json

{
  "slug": "early-access",
  "display_name": "Early Access Program",
  "description": "string",
  "settings": {},
  "status": "draft",
  "allowed_origins": [
    "https://acme.com",
    "https://*.acme.com"
  ]
}

Response

{
  "id": "b1a4...",
  "workspace_id": "b1a4...",
  "workspace_slug": "acme",
  "slug": "early-access",
  "display_name": "Early Access Program",
  "description": "string",
  "status": "draft",
  "accepting_signups": false,
  "signups_note": "This waitlist is a draft, so public sign-ups are refused (422) until you change its status to active.",
  "allowed_origins": [],
  "settings": {},
  "created_by": "string",
  "previous_workspace_slug": "string",
  "created_at": "string",
  "updated_at": "string"
}
get/v1/workspaces/{workspace_id}/waitlists/{id}Auth

Get waitlist details

Path parameters

id*string
workspace_id*string · uuid

Response · 200 Waitlist row.

id*string

Example: "b1a4..."

workspace_id*string

Example: "b1a4..."

workspace_slug*string

Example: "acme"

slug*string

Example: "early-access"

display_name*string

Example: "Early Access Program"

description*string
status*enum (4)
accepting_signups*boolean

Whether the public sign-up endpoint accepts entries right now (only an `active` waitlist does).

Example: false

signups_note*string

Plain explanation when public sign-ups are refused, and how to open them; null when the waitlist is active.

Example: "This waitlist is a draft, so public sign-ups are refused (422) until you change its status to active."

allowed_origins*array

Web origins (scheme and host, for example `https://acme.com` or `http://localhost:3000`) that browsers may submit public sign-ups from. `https://*.acme.com` matches any subdomain. An empty list means any origin. A browser request from another origin gets 403 `ORIGIN_NOT_ALLOWED`; server-side calls without an `Origin` header are not checked. Entries are normalised (lower case, no trailing slash, default port dropped).

Example: []

settings*object

Waitlist settings (max_entries, approval_required, custom_fields, confirmation_message, referral_boost, referral_rewards, recaptcha_site_key). The reCAPTCHA secret key is never returned; recaptcha_secret_configured says whether one is saved.

created_by*string
previous_workspace_slug*string
created_at*string
updated_at*string

Example

Request

GET /v1/workspaces/{workspace_id}/waitlists/{id}
Authorization: Bearer YOUR_TOKEN

Response

{
  "id": "b1a4...",
  "workspace_id": "b1a4...",
  "workspace_slug": "acme",
  "slug": "early-access",
  "display_name": "Early Access Program",
  "description": "string",
  "status": "draft",
  "accepting_signups": false,
  "signups_note": "This waitlist is a draft, so public sign-ups are refused (422) until you change its status to active.",
  "allowed_origins": [],
  "settings": {},
  "created_by": "string",
  "previous_workspace_slug": "string",
  "created_at": "string",
  "updated_at": "string"
}
patch/v1/workspaces/{workspace_id}/waitlists/{id}Auth

Update waitlist metadata and settings

Path parameters

id*string
workspace_id*string · uuid

Request body

display_namestring

New display name

descriptionstring

New description

settingsobject

Updated settings, merged shallowly into the saved ones. Keys: max_entries, approval_required, custom_fields, confirmation_message, referral_boost, referral_rewards, recaptcha_site_key. To turn on reCAPTCHA v3, send recaptcha_site_key together with recaptcha_secret_key (both from your reCAPTCHA admin console); the secret is stored encrypted and is never returned. Send recaptcha_site_key: null to turn it off.

allowed_originsarray

Web origins (scheme and host, for example `https://acme.com` or `http://localhost:3000`) that browsers may submit public sign-ups from. `https://*.acme.com` matches any subdomain. An empty list means any origin. A browser request from another origin gets 403 `ORIGIN_NOT_ALLOWED`; server-side calls without an `Origin` header are not checked. Entries are normalised (lower case, no trailing slash, default port dropped). Replaces the whole list; send `[]` to allow any origin again.

Example: ["https://acme.com"]

Response · 200 Waitlist updated.

id*string

Example: "b1a4..."

workspace_id*string

Example: "b1a4..."

workspace_slug*string

Example: "acme"

slug*string

Example: "early-access"

display_name*string

Example: "Early Access Program"

description*string
status*enum (4)
accepting_signups*boolean

Whether the public sign-up endpoint accepts entries right now (only an `active` waitlist does).

Example: false

signups_note*string

Plain explanation when public sign-ups are refused, and how to open them; null when the waitlist is active.

Example: "This waitlist is a draft, so public sign-ups are refused (422) until you change its status to active."

allowed_origins*array

Web origins (scheme and host, for example `https://acme.com` or `http://localhost:3000`) that browsers may submit public sign-ups from. `https://*.acme.com` matches any subdomain. An empty list means any origin. A browser request from another origin gets 403 `ORIGIN_NOT_ALLOWED`; server-side calls without an `Origin` header are not checked. Entries are normalised (lower case, no trailing slash, default port dropped).

Example: []

settings*object

Waitlist settings (max_entries, approval_required, custom_fields, confirmation_message, referral_boost, referral_rewards, recaptcha_site_key). The reCAPTCHA secret key is never returned; recaptcha_secret_configured says whether one is saved.

created_by*string
previous_workspace_slug*string
created_at*string
updated_at*string

Example

Request

PATCH /v1/workspaces/{workspace_id}/waitlists/{id}
Authorization: Bearer YOUR_TOKEN
Content-Type: application/json

{
  "display_name": "string",
  "description": "string",
  "settings": {},
  "allowed_origins": [
    "https://acme.com"
  ]
}

Response

{
  "id": "b1a4...",
  "workspace_id": "b1a4...",
  "workspace_slug": "acme",
  "slug": "early-access",
  "display_name": "Early Access Program",
  "description": "string",
  "status": "draft",
  "accepting_signups": false,
  "signups_note": "This waitlist is a draft, so public sign-ups are refused (422) until you change its status to active.",
  "allowed_origins": [],
  "settings": {},
  "created_by": "string",
  "previous_workspace_slug": "string",
  "created_at": "string",
  "updated_at": "string"
}
delete/v1/workspaces/{workspace_id}/waitlists/{id}Auth

Delete a waitlist (cascades to entries)

Path parameters

id*string
workspace_id*string · uuid

Response · 204

Waitlist deleted.

patch/v1/workspaces/{workspace_id}/waitlists/{id}/statusAuth

Change waitlist status

Path parameters

id*string
workspace_id*string · uuid

Request body

status*enum (4)

New status

Response · 200 Status updated.

id*string

Example: "b1a4..."

workspace_id*string

Example: "b1a4..."

workspace_slug*string

Example: "acme"

slug*string

Example: "early-access"

display_name*string

Example: "Early Access Program"

description*string
status*enum (4)
accepting_signups*boolean

Whether the public sign-up endpoint accepts entries right now (only an `active` waitlist does).

Example: false

signups_note*string

Plain explanation when public sign-ups are refused, and how to open them; null when the waitlist is active.

Example: "This waitlist is a draft, so public sign-ups are refused (422) until you change its status to active."

allowed_origins*array

Web origins (scheme and host, for example `https://acme.com` or `http://localhost:3000`) that browsers may submit public sign-ups from. `https://*.acme.com` matches any subdomain. An empty list means any origin. A browser request from another origin gets 403 `ORIGIN_NOT_ALLOWED`; server-side calls without an `Origin` header are not checked. Entries are normalised (lower case, no trailing slash, default port dropped).

Example: []

settings*object

Waitlist settings (max_entries, approval_required, custom_fields, confirmation_message, referral_boost, referral_rewards, recaptcha_site_key). The reCAPTCHA secret key is never returned; recaptcha_secret_configured says whether one is saved.

created_by*string
previous_workspace_slug*string
created_at*string
updated_at*string

Example

Request

PATCH /v1/workspaces/{workspace_id}/waitlists/{id}/status
Authorization: Bearer YOUR_TOKEN
Content-Type: application/json

{
  "status": "draft"
}

Response

{
  "id": "b1a4...",
  "workspace_id": "b1a4...",
  "workspace_slug": "acme",
  "slug": "early-access",
  "display_name": "Early Access Program",
  "description": "string",
  "status": "draft",
  "accepting_signups": false,
  "signups_note": "This waitlist is a draft, so public sign-ups are refused (422) until you change its status to active.",
  "allowed_origins": [],
  "settings": {},
  "created_by": "string",
  "previous_workspace_slug": "string",
  "created_at": "string",
  "updated_at": "string"
}

Entries

get/v1/workspaces/{workspace_id}/waitlists/{waitlist_id}/entriesAuth

List entries on a waitlist (queue-position order, cursor-paginated). Filterable by status, free-text search across email/name, and a created_at window.

Path parameters

workspace_id*string
waitlist_id*string

Query parameters

limitnumber
cursorstring

Opaque cursor for pagination

statusstring

Filter by status

searchstring

Case-insensitive substring match across the entry `email` OR `name`. Trimmed; blank means no filter. Composes (AND) with `status`, `since` and `until`.

sincestring · date-time

ISO-8601 inclusive lower bound on `created_at`. Malformed values are rejected with a 400.

untilstring · date-time

ISO-8601 exclusive upper bound on `created_at`. Must be after `since`. Malformed values are rejected with a 400.

is_testboolean

`true` lists only test entries ("Add test signup"), `false` only real ones. Omitted lists both; test entries come first (negative positions).

Response · 200 Page of entries with pagination cursor.

data*array
pagination*object
next_cursor*string
has_more*boolean

Example

Request

GET /v1/workspaces/{workspace_id}/waitlists/{waitlist_id}/entries
Authorization: Bearer YOUR_TOKEN

Response

{
  "data": [
    {
      "id": "string",
      "waitlist_id": "string",
      "workspace_id": "string",
      "email": "string",
      "name": "string",
      "interest": "string",
      "referrer": "string",
      "referral_code": "string",
      "referral_count": 0,
      "referred_by_entry_id": "string",
      "position": 0,
      "is_test": false,
      "status": "pending",
      "invited_at": "string",
      "invited_to_app_slug": "string",
      "metadata": {},
      "created_at": "string",
      "updated_at": "string"
    }
  ],
  "pagination": {
    "next_cursor": "string",
    "has_more": false
  }
}
get/v1/workspaces/{workspace_id}/waitlists/{waitlist_id}/entries/countAuth

Entry count for a waitlist (test entries left out unless include_test=true)

Path parameters

workspace_id*string
waitlist_id*string

Query parameters

include_testboolean

Count test entries ("Add test signup") too. Default `false`: they are left out.

Response · 200 Entry count.

count*number

Number of entries on the waitlist. Test entries are left out unless `include_test=true`.

Example

Request

GET /v1/workspaces/{workspace_id}/waitlists/{waitlist_id}/entries/count
Authorization: Bearer YOUR_TOKEN

Response

{
  "count": 0
}
get/v1/workspaces/{workspace_id}/waitlists/{waitlist_id}/entries/export.csvAuth

Stream every entry as CSV (admin export; test entries left out unless include_test=true)

Path parameters

workspace_id*string
waitlist_id*string

Query parameters

include_testboolean

Count test entries ("Add test signup") too. Default `false`: they are left out.

Response · 200

text/csv stream — one header row + one row per entry. Content-Disposition: attachment.

get/v1/workspaces/{workspace_id}/waitlists/{waitlist_id}/entries/{entry_id}Auth

Get a single entry

Path parameters

workspace_id*string
waitlist_id*string
entry_id*string

Response · 200 Entry row.

id*string
waitlist_id*string
workspace_id*string
email*string
name*string
interest*string
referrer*string
referral_code*string
referral_count*number
referred_by_entry_id*string
position*number

Place in the queue, from 1. Test entries have negative positions (-1, -2…) so they never take a real place.

is_test*boolean

Made with "Add test signup". Test entries use a mailbox-simulator address and are left out of counts, analytics, exports and webhooks unless asked.

Example: false

status*enum (3)
invited_at*string
invited_to_app_slug*string
metadata*object
created_at*string
updated_at*string

Example

Request

GET /v1/workspaces/{workspace_id}/waitlists/{waitlist_id}/entries/{entry_id}
Authorization: Bearer YOUR_TOKEN

Response

{
  "id": "string",
  "waitlist_id": "string",
  "workspace_id": "string",
  "email": "string",
  "name": "string",
  "interest": "string",
  "referrer": "string",
  "referral_code": "string",
  "referral_count": 0,
  "referred_by_entry_id": "string",
  "position": 0,
  "is_test": false,
  "status": "pending",
  "invited_at": "string",
  "invited_to_app_slug": "string",
  "metadata": {},
  "created_at": "string",
  "updated_at": "string"
}
patch/v1/workspaces/{workspace_id}/waitlists/{waitlist_id}/entries/{entry_id}Auth

Approve or reject an entry

Path parameters

workspace_id*string
waitlist_id*string
entry_id*string

Request body

status*enum (2)

Target status

Response · 200 Entry status updated.

id*string
waitlist_id*string
workspace_id*string
email*string
name*string
interest*string
referrer*string
referral_code*string
referral_count*number
referred_by_entry_id*string
position*number

Place in the queue, from 1. Test entries have negative positions (-1, -2…) so they never take a real place.

is_test*boolean

Made with "Add test signup". Test entries use a mailbox-simulator address and are left out of counts, analytics, exports and webhooks unless asked.

Example: false

status*enum (3)
invited_at*string
invited_to_app_slug*string
metadata*object
created_at*string
updated_at*string

Example

Request

PATCH /v1/workspaces/{workspace_id}/waitlists/{waitlist_id}/entries/{entry_id}
Authorization: Bearer YOUR_TOKEN
Content-Type: application/json

{
  "status": "approved"
}

Response

{
  "id": "string",
  "waitlist_id": "string",
  "workspace_id": "string",
  "email": "string",
  "name": "string",
  "interest": "string",
  "referrer": "string",
  "referral_code": "string",
  "referral_count": 0,
  "referred_by_entry_id": "string",
  "position": 0,
  "is_test": false,
  "status": "pending",
  "invited_at": "string",
  "invited_to_app_slug": "string",
  "metadata": {},
  "created_at": "string",
  "updated_at": "string"
}
delete/v1/workspaces/{workspace_id}/waitlists/{waitlist_id}/entries/{entry_id}Auth

Delete an entry (positions are not renumbered)

Path parameters

workspace_id*string
waitlist_id*string
entry_id*string

Response · 204

Entry deleted.

post/v1/workspaces/{workspace_id}/waitlists/{waitlist_id}/entries/{entry_id}/invite-to-appAuth

Invite an approved entry into a linked Auth consumer app (one-click cross-product handoff)

Path parameters

workspace_id*string
waitlist_id*string
entry_id*string

Request body

app_slugstring

Auth app slug to invite into; defaults to the workspace Waitlist settings default (`default_heimdall_app_slug`)

send_emailboolean

Send the approval email with the Auth invite link baked in

Response · 200 Invite created in Auth

entry*object

The entry row after `invited_at` / `invited_to_app_slug` were stamped.

invite_id*string

Auth invite id created in the linked app.

invite_urlstring

Invite acceptance URL, when the Auth app returned one. Baked into the approval email when send_email is enabled.

Example

Request

POST /v1/workspaces/{workspace_id}/waitlists/{waitlist_id}/entries/{entry_id}/invite-to-app
Authorization: Bearer YOUR_TOKEN
Content-Type: application/json

{
  "app_slug": "string",
  "send_email": false
}

Response

{
  "entry": {},
  "invite_id": "string",
  "invite_url": "string"
}
post/v1/workspaces/{workspace_id}/waitlists/{waitlist_id}/entries/bulkAuth

Bulk-apply approve / reject / invite-to-app to multiple entries at once. Capped at 100 ids per call. Processed serially server-side; per-row results are returned so the UI can render partial successes.

Path parameters

workspace_id*string
waitlist_id*string

Request body

ids*array

Entry UUIDs to act on in this batch. Capped at 100 per call; the request is processed serially and per-row results are returned. De-duped server-side.

Example: ["11111111-1111-1111-1111-111111111111","22222222-2222-2222-2222-222222222222"]

action*enum (3)

Action to apply to every id.

Example: "approve"

app_slugstring

Auth app slug override for `action: invite-to-app`. When omitted, falls back to the workspace Waitlist settings default (`default_heimdall_app_slug`). Ignored for approve/reject.

Example: "acme-product"

send_emailboolean

Whether to send the approval email with the invite link baked in. Defaults to `true`. Ignored for approve/reject.

Response · 200 `{ requested, succeeded, failed, results }`. Each row in `results` carries `{ id, status: "ok" | "error", error?: { code, message } }` so a partial failure (one bad id) doesn't fail the whole batch.

requested*number

Number of unique ids processed (after de-dupe).

succeeded*number
failed*number
results*array

Example

Request

POST /v1/workspaces/{workspace_id}/waitlists/{waitlist_id}/entries/bulk
Authorization: Bearer YOUR_TOKEN
Content-Type: application/json

{
  "ids": [
    "11111111-1111-1111-1111-111111111111",
    "22222222-2222-2222-2222-222222222222"
  ],
  "action": "approve",
  "app_slug": "acme-product",
  "send_email": false
}

Response

{
  "requested": 0,
  "succeeded": 0,
  "failed": 0,
  "results": [
    {
      "id": "00000000-0000-0000-0000-000000000000",
      "status": "ok",
      "error": {
        "code": 0,
        "message": "string"
      }
    }
  ]
}
post/v1/workspaces/{workspace_id}/waitlists/{waitlist_id}/test-entriesAuth

Add a test signup

Creates an entry marked `is_test` with a generated Amazon SES mailbox-simulator address (`success+test-…@simulator.amazonses.com`). Works on draft and paused waitlists and ignores `max_entries`. The entry shows in the entries list, takes a negative position, and is left out of counts, analytics, exports and webhooks unless asked. No email is sent unless `send_welcome_email` is true. Delete it like any other entry.

Path parameters

workspace_id*string
waitlist_id*string

Headers

Idempotency-Keystring

Idempotency key (24 h). A retry with the same key returns the first test entry instead of adding a second one.

Request body

namestring

Name on the test entry. Defaults to "Test signup".

Example: "Test signup"

trigger_webhooksboolean

Send `entry.created` (and `entry.approved` when the waitlist auto-approves) to your webhook endpoints for this entry. The payload carries `is_test: true`. Default `false`.

send_welcome_emailboolean

Send the welcome email for this entry. It goes to the entry's mailbox-simulator address, which accepts it without delivering to a real inbox. Default `false`.

Response · 201 Test entry created.

id*string
waitlist_id*string
workspace_id*string
email*string
name*string
interest*string
referrer*string
referral_code*string
referral_count*number
referred_by_entry_id*string
position*number

Place in the queue, from 1. Test entries have negative positions (-1, -2…) so they never take a real place.

is_test*boolean

Made with "Add test signup". Test entries use a mailbox-simulator address and are left out of counts, analytics, exports and webhooks unless asked.

Example: false

status*enum (3)
invited_at*string
invited_to_app_slug*string
metadata*object
created_at*string
updated_at*string

Example

Request

POST /v1/workspaces/{workspace_id}/waitlists/{waitlist_id}/test-entries
Authorization: Bearer YOUR_TOKEN
Content-Type: application/json

{
  "name": "Test signup",
  "trigger_webhooks": false,
  "send_welcome_email": false
}

Response

{
  "id": "string",
  "waitlist_id": "string",
  "workspace_id": "string",
  "email": "string",
  "name": "string",
  "interest": "string",
  "referrer": "string",
  "referral_code": "string",
  "referral_count": 0,
  "referred_by_entry_id": "string",
  "position": 0,
  "is_test": false,
  "status": "pending",
  "invited_at": "string",
  "invited_to_app_slug": "string",
  "metadata": {},
  "created_at": "string",
  "updated_at": "string"
}

Public-Waitlist

get/v1/waitlists/{workspace_slug}/{waitlist_slug}

Public waitlist info (display name, description, custom-field schema, branding hints, active variant)

Path parameters

workspace_slug*string
waitlist_slug*string

Query parameters

langstring

BCP-47 locale tag (e.g. `en`, `pt-BR`). Wins over Accept-Language when picking the variant.

Response · 200 Public waitlist info

slug*string

Waitlist slug.

workspace_slug*string

Workspace slug the request resolved against.

canonical_workspace_slug*string

Canonical workspace slug. When the request URL used a stale slug, compare with the URL slug and 301-redirect.

display_name*string

Display name (active variant copy wins over the stored name).

description*string
status*string

Always `active` — non-active waitlists return 404.

custom_fields*array
confirmation_message*string

Message to show after a successful signup.

recaptcha_site_key*string

reCAPTCHA v3 site key. When non-null, POST .../entries requires a `recaptcha_token` created with the action `waitlist_signup`.

referral_rewards*object
milestones*array

Milestones sorted by count, ascending. Empty when the waitlist has no referral rewards configured.

variant*object

Active A/B or locale variant served for this request; null when none configured.

brand*object
brand_name*string
brand_logo_url*string
primary_color*string

Primary brand colour as #RRGGBB.

Example

Request

GET /v1/waitlists/{workspace_slug}/{waitlist_slug}

Response

{
  "slug": "string",
  "workspace_slug": "string",
  "canonical_workspace_slug": "string",
  "display_name": "string",
  "description": "string",
  "status": "string",
  "custom_fields": [
    {
      "key": "string",
      "label": "string",
      "type": "string",
      "required": false,
      "options": [
        "string"
      ]
    }
  ],
  "confirmation_message": "string",
  "recaptcha_site_key": "string",
  "referral_rewards": {
    "milestones": [
      {
        "count": 0,
        "reward": "string"
      }
    ]
  },
  "variant": {},
  "brand": {
    "brand_name": "string",
    "brand_logo_url": "string",
    "primary_color": "string"
  }
}
get/v1/waitlists/{workspace_slug}/{waitlist_slug}/leaderboard

Public top-50 referral leaderboard. Emails are masked. Anonymous read; same access policy as the public waitlist info endpoint.

Path parameters

workspace_slug*string
waitlist_slug*string

Query parameters

limitstring

Number of rows (1-50, default 50)

Response · 200 Array of `{ position, referral_count, masked_email }`

Array of object

position*number

Signup position on the waitlist.

referral_count*number

Number of honoured referrals.

masked_email*string

Masked email, e.g. `a***@example.com`.

Example: "a***@example.com"

Example

Request

GET /v1/waitlists/{workspace_slug}/{waitlist_slug}/leaderboard

Response

[
  {
    "position": 0,
    "referral_count": 0,
    "masked_email": "a***@example.com"
  }
]
post/v1/waitlists/{workspace_slug}/{waitlist_slug}/entries

Submit a new entry to a public waitlist

Path parameters

workspace_slug*string
waitlist_slug*string

Request body

email*string

Email address

Example: "ada@example.com"

name*string

Full name

Example: "Ada Lovelace"

intereststring

Free-text product interest

referrerstring

How they heard about it

referral_codestring

Referral code from a prior signup; bumps that signup’s position when honoured

metadataobject

Custom field values

recaptcha_tokenstring

reCAPTCHA v3 token, created with the action `waitlist_signup`. Required when the waitlist has a reCAPTCHA site key; verified server-side with the secret key saved on the waitlist.

variant_idstring · uuid

Variant id this submission was attributed to. Customer frontends fetch the active variant from `GET /v1/waitlists/:workspaceSlug/:waitlistSlug` and round-trip it here on POST so per-variant conversion can be computed without a separate impressions table.

Response · 201 Entry created. Persist `referral_code` client-side to build the referral URL; `status` is `approved` when the waitlist auto-approves. A sign-up from an Amazon SES mailbox-simulator address (`…@simulator.amazonses.com`) is stored as a test entry (`is_test: true`, negative `position`), so test runs never land on the real list.

id*string · uuid

Entry id.

email*string

Normalised (lower-cased) email.

name*string
position*number

Immutable signup position (1-based order of signup). Negative for a test sign-up (`is_test: true`), which takes no place in line.

is_test*boolean

`true` when the email is an Amazon SES mailbox-simulator address (`…@simulator.amazonses.com`). Such a sign-up is stored as a test entry: it is left out of counts, analytics, exports and webhooks, and only the welcome email (to the simulator) goes out.

status*enum (2)

`approved` when the waitlist auto-approves; `pending` when admin approval is required.

referral_code*string

Referral code for this signup. Build your referral URL from it and pass it as `referral_code` on subsequent signups.

created_at*string · date-time
effective_position*number

Visible position after applying the referral boost — "you would be #N if approved now". Equals `position` at signup time (no referrals yet), and always for a test sign-up.

Example

Request

POST /v1/waitlists/{workspace_slug}/{waitlist_slug}/entries
Content-Type: application/json

{
  "email": "ada@example.com",
  "name": "Ada Lovelace",
  "interest": "string",
  "referrer": "string",
  "referral_code": "string",
  "metadata": {},
  "recaptcha_token": "string",
  "variant_id": "00000000-0000-0000-0000-000000000000"
}

Response

{
  "id": "00000000-0000-0000-0000-000000000000",
  "email": "string",
  "name": "string",
  "position": 0,
  "is_test": false,
  "status": "pending",
  "referral_code": "string",
  "created_at": "2026-01-01T00:00:00.000Z",
  "effective_position": 0
}

Analytics

get/v1/workspaces/{workspace_id}/waitlists/{waitlist_id}/analyticsAuth

Aggregate counts, viral coefficient, top referrers + referrals

Test entries ("Add test signup") are left out unless `include_test=true`.

Path parameters

workspace_id*string
waitlist_id*string

Query parameters

include_testboolean

Count test entries ("Add test signup") too. Default `false`: they are left out.

Response · 200 Aggregated analytics payload for the waitlist.

waitlist_id*string · uuid
total_entries*number
by_status*object

Entry counts keyed by status (`pending` / `approved` / `rejected`).

Example: {"pending":12,"approved":40,"rejected":3}

signups_today*number
signups_this_week*number
signups_this_month*number
viral_coefficient*number

referrals / total_entries — the K-factor proxy. 0 when no entries.

referred_count*number
top_referrers*array
top_referrals*array

Example

Request

GET /v1/workspaces/{workspace_id}/waitlists/{waitlist_id}/analytics
Authorization: Bearer YOUR_TOKEN

Response

{
  "waitlist_id": "00000000-0000-0000-0000-000000000000",
  "total_entries": 0,
  "by_status": {
    "pending": 12,
    "approved": 40,
    "rejected": 3
  },
  "signups_today": 0,
  "signups_this_week": 0,
  "signups_this_month": 0,
  "viral_coefficient": 0,
  "referred_count": 0,
  "top_referrers": [
    {
      "referrer": "string",
      "count": 0
    }
  ],
  "top_referrals": [
    {
      "id": "00000000-0000-0000-0000-000000000000",
      "email": "string",
      "name": "string",
      "referral_code": "string",
      "referral_count": 0,
      "position": 0,
      "effective_position": 0
    }
  ]
}
get/v1/workspaces/{workspace_id}/waitlists/{waitlist_id}/analytics/timelineAuth

Daily-bucketed signup timeline (UTC, dense — zero-count days included)

Path parameters

workspace_id*string
waitlist_id*string

Query parameters

since*string

ISO-8601 inclusive start

until*string

ISO-8601 exclusive end

include_testboolean

Count test entries ("Add test signup") too. Default `false`: they are left out.

Response · 200 Daily timeline array, oldest first, zero-count days included.

granularity*enum (1)
points*array

Oldest first; every day in [since, until) is present.

Example

Request

GET /v1/workspaces/{workspace_id}/waitlists/{waitlist_id}/analytics/timeline
Authorization: Bearer YOUR_TOKEN

Response

{
  "granularity": "day",
  "points": [
    {
      "date": "2026-08-01",
      "count": 0
    }
  ]
}

Waitlist-Variants

get/v1/workspaces/{workspace_id}/waitlists/{waitlist_id}/variantsAuth

List variants on a waitlist (cursor-paginated). BREAKING: this endpoint used to return a bare JSON array; it now returns the standard `{ data, pagination }` envelope.

Path parameters

workspace_id*string
waitlist_id*string

Query parameters

limitinteger

Page size. Defaults to 20; values above 100 are clamped to 100.

cursorstring

Opaque cursor from the previous page's `pagination.next_cursor`. Omit for the first page.

Response · 200 Page of variants in (kind, locale, created_at) order with a pagination cursor.

data*array
pagination*object
next_cursor*string

Opaque cursor for the next page. `null` when no more pages are available.

Example: "eyJwb3NpdGlvbiI6MjB9"

has_more*boolean

True iff another page exists; if true, pass `next_cursor` on the next request.

Example: false

Example

Request

GET /v1/workspaces/{workspace_id}/waitlists/{waitlist_id}/variants
Authorization: Bearer YOUR_TOKEN

Response

{
  "data": [
    {
      "id": "string",
      "waitlist_id": "string",
      "kind": "ab",
      "locale": "string",
      "copy": {},
      "weight": 0,
      "active": false,
      "created_at": "string"
    }
  ],
  "pagination": {
    "next_cursor": "eyJwb3NpdGlvbiI6MjB9",
    "has_more": false
  }
}
post/v1/workspaces/{workspace_id}/waitlists/{waitlist_id}/variantsAuth

Create a variant

Path parameters

workspace_id*string
waitlist_id*string

Request body

kind*enum (2)
locale*string

BCP-47 locale tag. For ab variants this is the locale they target; for locale variants this is the locale they serve. Case is normalised on the public side.

Example: "en"

copy*object

Copy overrides. Recognised keys: display_name, description, confirmation_message. Unknown keys are dropped on the public render.

weightnumber

Relative weight inside the ab pool. Ignored when kind=locale. Default 1.

activeboolean

Response · 201 Variant created

id*string
waitlist_id*string
kind*enum (2)
locale*string
copy*object
weight*number
active*boolean
created_at*string

Example

Request

POST /v1/workspaces/{workspace_id}/waitlists/{waitlist_id}/variants
Authorization: Bearer YOUR_TOKEN
Content-Type: application/json

{
  "kind": "ab",
  "locale": "en",
  "copy": {},
  "weight": 0,
  "active": false
}

Response

{
  "id": "string",
  "waitlist_id": "string",
  "kind": "ab",
  "locale": "string",
  "copy": {},
  "weight": 0,
  "active": false,
  "created_at": "string"
}
patch/v1/workspaces/{workspace_id}/waitlists/{waitlist_id}/variants/{id}Auth

Update a variant (locale, copy, weight, active)

Path parameters

workspace_id*string
waitlist_id*string
id*string

Request body

localestring
copyobject
display_namestring
descriptionstring
confirmation_messagestring
weightnumber
activeboolean

Response · 200 Variant updated

id*string
waitlist_id*string
kind*enum (2)
locale*string
copy*object
weight*number
active*boolean
created_at*string

Example

Request

PATCH /v1/workspaces/{workspace_id}/waitlists/{waitlist_id}/variants/{id}
Authorization: Bearer YOUR_TOKEN
Content-Type: application/json

{
  "locale": "string",
  "copy": {
    "display_name": "string",
    "description": "string",
    "confirmation_message": "string"
  },
  "weight": 0,
  "active": false
}

Response

{
  "id": "string",
  "waitlist_id": "string",
  "kind": "ab",
  "locale": "string",
  "copy": {},
  "weight": 0,
  "active": false,
  "created_at": "string"
}
delete/v1/workspaces/{workspace_id}/waitlists/{waitlist_id}/variants/{id}Auth

Delete a variant

Path parameters

workspace_id*string
waitlist_id*string
id*string

Response · 204

Variant deleted


Webhooks

get/v1/workspaces/{workspace_id}/webhooks/event-typesAuth

List the events an endpoint can subscribe to.

Each type with a plain description, its group and an example `data` object (what "Send test" delivers).

Path parameters

workspace_id*string

Response · 200

data*array

Example

Request

GET /v1/workspaces/{workspace_id}/webhooks/event-types
Authorization: Bearer YOUR_TOKEN

Response

{
  "data": [
    {
      "type": "entry.created",
      "description": "A user signed up or was created by an admin.",
      "category": "Users",
      "example": {}
    }
  ]
}
get/v1/workspaces/{workspace_id}/webhooks/endpointsAuth

List webhook endpoints, oldest first.

At most 10, so this is always one page. Signing secrets are never returned here, only their last 4 characters.

Path parameters

workspace_id*string

Response · 200

data*array
pagination*object

Always one page (an endpoint limit applies).

Example

Request

GET /v1/workspaces/{workspace_id}/webhooks/endpoints
Authorization: Bearer YOUR_TOKEN

Response

{
  "data": [
    {
      "id": "00000000-0000-0000-0000-000000000000",
      "scope_type": "workspace",
      "scope_id": "string",
      "url": "https://api.acme.com/webhooks",
      "description": "Production receiver",
      "event_types": [],
      "enabled": false,
      "status": "enabled",
      "disabled_at": "2026-01-01T00:00:00.000Z",
      "disabled_reason": "manual",
      "consecutive_failures": 0,
      "first_failure_at": "2026-01-01T00:00:00.000Z",
      "signing_secret_hint": "x9Q=",
      "prev_signing_secret_active": false,
      "prev_signing_secret_expires_at": "2026-01-01T00:00:00.000Z",
      "legacy_signature": false,
      "created_at": "2026-01-01T00:00:00.000Z",
      "updated_at": "2026-01-01T00:00:00.000Z"
    }
  ],
  "pagination": {}
}
post/v1/workspaces/{workspace_id}/webhooks/endpointsAuth

Add a webhook endpoint. Returns its signing secret once.

Deliveries are signed with the Standard Webhooks headers `webhook-id`, `webhook-timestamp` and `webhook-signature` (`v1,<base64 HMAC-SHA256 of "<id>.<timestamp>.<body>">`); the returned `whsec_…` secret works unchanged with any Standard Webhooks library. Failed deliveries are retried after 1 min, 5 min, 30 min, 2 h, 6 h and 24 h.

Path parameters

workspace_id*string

Headers

Idempotency-Keystring

Opaque client-generated string (1-256 chars, `[A-Za-z0-9_-:]`) that makes a retry of this request safe for 24 hours. The same key with a different body returns 409 `IDEMPOTENCY_KEY_REUSE`.

Request body

url*string · uri

Where to send events: an absolute https:// URL on a public host.

Example: "https://api.acme.com/webhooks"

descriptionstring

Example: "Production receiver"

event_typesarray

Event types from `GET …/webhooks/event-types`. Empty or omitted means every event, including types added later.

Example: ["entry.created","entry.approved"]

enabledboolean

Create it switched off.

Response · 201

id*string · uuid
scope_type*string

What owns the endpoint: `workspace`, `app` or `community`.

Example: "workspace"

scope_id*string

Id of the owning workspace, app or community.

url*string · uri

Example: "https://api.acme.com/webhooks"

description*string

Example: "Production receiver"

event_types*array

Subscribed event types. Empty means every event, including types added later.

Example: []

enabled*boolean

The on/off switch (`PATCH … { enabled }`).

status*enum (3)

`enabled` receives events. `disabled` was switched off. `auto_disabled` was switched off after 20 failed deliveries in a row or 24 hours of failures; `PATCH { enabled: true }` turns it back on.

disabled_at*string · date-time
disabled_reason*enum (2)
consecutive_failures*number

Failed automatic deliveries in a row; a success resets it.

first_failure_at*string · date-time

Start of the current failure streak.

signing_secret_hint*string

Last 4 characters of the signing secret.

Example: "x9Q="

prev_signing_secret_active*boolean

True while the previous secret still signs too (after a rotation, for its grace period).

prev_signing_secret_expires_at*string · date-time

Until then the previous secret signs too (after a rotation).

legacy_signature*boolean

True for endpoints carried over from the product's previous webhook system: they also receive the old signature header until its sunset date.

created_at*string · date-time
updated_at*string · date-time
signing_secret*string

The signing secret, shown once: pass it unchanged to any Standard Webhooks verifier. Store it now; it cannot be shown again.

Example: "whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw"

Example

Request

POST /v1/workspaces/{workspace_id}/webhooks/endpoints
Authorization: Bearer YOUR_TOKEN
Content-Type: application/json

{
  "url": "https://api.acme.com/webhooks",
  "description": "Production receiver",
  "event_types": [
    "entry.created",
    "entry.approved"
  ],
  "enabled": false
}

Response

{
  "id": "00000000-0000-0000-0000-000000000000",
  "scope_type": "workspace",
  "scope_id": "string",
  "url": "https://api.acme.com/webhooks",
  "description": "Production receiver",
  "event_types": [],
  "enabled": false,
  "status": "enabled",
  "disabled_at": "2026-01-01T00:00:00.000Z",
  "disabled_reason": "manual",
  "consecutive_failures": 0,
  "first_failure_at": "2026-01-01T00:00:00.000Z",
  "signing_secret_hint": "x9Q=",
  "prev_signing_secret_active": false,
  "prev_signing_secret_expires_at": "2026-01-01T00:00:00.000Z",
  "legacy_signature": false,
  "created_at": "2026-01-01T00:00:00.000Z",
  "updated_at": "2026-01-01T00:00:00.000Z",
  "signing_secret": "whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw"
}
get/v1/workspaces/{workspace_id}/webhooks/endpoints/{endpoint_id}Auth

Get one webhook endpoint (without its secret).

Path parameters

workspace_id*string
endpoint_id*string · uuid

Response · 200

id*string · uuid
scope_type*string

What owns the endpoint: `workspace`, `app` or `community`.

Example: "workspace"

scope_id*string

Id of the owning workspace, app or community.

url*string · uri

Example: "https://api.acme.com/webhooks"

description*string

Example: "Production receiver"

event_types*array

Subscribed event types. Empty means every event, including types added later.

Example: []

enabled*boolean

The on/off switch (`PATCH … { enabled }`).

status*enum (3)

`enabled` receives events. `disabled` was switched off. `auto_disabled` was switched off after 20 failed deliveries in a row or 24 hours of failures; `PATCH { enabled: true }` turns it back on.

disabled_at*string · date-time
disabled_reason*enum (2)
consecutive_failures*number

Failed automatic deliveries in a row; a success resets it.

first_failure_at*string · date-time

Start of the current failure streak.

signing_secret_hint*string

Last 4 characters of the signing secret.

Example: "x9Q="

prev_signing_secret_active*boolean

True while the previous secret still signs too (after a rotation, for its grace period).

prev_signing_secret_expires_at*string · date-time

Until then the previous secret signs too (after a rotation).

legacy_signature*boolean

True for endpoints carried over from the product's previous webhook system: they also receive the old signature header until its sunset date.

created_at*string · date-time
updated_at*string · date-time

Example

Request

GET /v1/workspaces/{workspace_id}/webhooks/endpoints/{endpoint_id}
Authorization: Bearer YOUR_TOKEN

Response

{
  "id": "00000000-0000-0000-0000-000000000000",
  "scope_type": "workspace",
  "scope_id": "string",
  "url": "https://api.acme.com/webhooks",
  "description": "Production receiver",
  "event_types": [],
  "enabled": false,
  "status": "enabled",
  "disabled_at": "2026-01-01T00:00:00.000Z",
  "disabled_reason": "manual",
  "consecutive_failures": 0,
  "first_failure_at": "2026-01-01T00:00:00.000Z",
  "signing_secret_hint": "x9Q=",
  "prev_signing_secret_active": false,
  "prev_signing_secret_expires_at": "2026-01-01T00:00:00.000Z",
  "legacy_signature": false,
  "created_at": "2026-01-01T00:00:00.000Z",
  "updated_at": "2026-01-01T00:00:00.000Z"
}
patch/v1/workspaces/{workspace_id}/webhooks/endpoints/{endpoint_id}Auth

Change a webhook endpoint: URL, description, events, on/off.

Only the fields sent change. `enabled: true` (or a new URL) also clears an automatic disable. Switching off cancels queued retries. The secret is unchanged; rotate it with `POST …/rotate-secret`.

Path parameters

workspace_id*string
endpoint_id*string · uuid

Headers

Idempotency-Keystring

Opaque client-generated string (1-256 chars, `[A-Za-z0-9_-:]`) that makes a retry of this request safe for 24 hours. The same key with a different body returns 409 `IDEMPOTENCY_KEY_REUSE`.

Request body

urlstring · uri

Example: "https://api.acme.com/webhooks"

descriptionstring
event_typesarray

Replaces the subscription. Event types from `GET …/webhooks/event-types`. Empty or omitted means every event, including types added later.

enabledboolean

Switch it on or off. Switching on also clears an automatic disable.

Response · 200

id*string · uuid
scope_type*string

What owns the endpoint: `workspace`, `app` or `community`.

Example: "workspace"

scope_id*string

Id of the owning workspace, app or community.

url*string · uri

Example: "https://api.acme.com/webhooks"

description*string

Example: "Production receiver"

event_types*array

Subscribed event types. Empty means every event, including types added later.

Example: []

enabled*boolean

The on/off switch (`PATCH … { enabled }`).

status*enum (3)

`enabled` receives events. `disabled` was switched off. `auto_disabled` was switched off after 20 failed deliveries in a row or 24 hours of failures; `PATCH { enabled: true }` turns it back on.

disabled_at*string · date-time
disabled_reason*enum (2)
consecutive_failures*number

Failed automatic deliveries in a row; a success resets it.

first_failure_at*string · date-time

Start of the current failure streak.

signing_secret_hint*string

Last 4 characters of the signing secret.

Example: "x9Q="

prev_signing_secret_active*boolean

True while the previous secret still signs too (after a rotation, for its grace period).

prev_signing_secret_expires_at*string · date-time

Until then the previous secret signs too (after a rotation).

legacy_signature*boolean

True for endpoints carried over from the product's previous webhook system: they also receive the old signature header until its sunset date.

created_at*string · date-time
updated_at*string · date-time

Example

Request

PATCH /v1/workspaces/{workspace_id}/webhooks/endpoints/{endpoint_id}
Authorization: Bearer YOUR_TOKEN
Content-Type: application/json

{
  "url": "https://api.acme.com/webhooks",
  "description": "string",
  "event_types": [
    "entry.created"
  ],
  "enabled": false
}

Response

{
  "id": "00000000-0000-0000-0000-000000000000",
  "scope_type": "workspace",
  "scope_id": "string",
  "url": "https://api.acme.com/webhooks",
  "description": "Production receiver",
  "event_types": [],
  "enabled": false,
  "status": "enabled",
  "disabled_at": "2026-01-01T00:00:00.000Z",
  "disabled_reason": "manual",
  "consecutive_failures": 0,
  "first_failure_at": "2026-01-01T00:00:00.000Z",
  "signing_secret_hint": "x9Q=",
  "prev_signing_secret_active": false,
  "prev_signing_secret_expires_at": "2026-01-01T00:00:00.000Z",
  "legacy_signature": false,
  "created_at": "2026-01-01T00:00:00.000Z",
  "updated_at": "2026-01-01T00:00:00.000Z"
}
delete/v1/workspaces/{workspace_id}/webhooks/endpoints/{endpoint_id}Auth

Delete a webhook endpoint.

Queued retries are dropped. Its past attempts stay in the event log until the events expire.

Path parameters

workspace_id*string
endpoint_id*string · uuid

Response · 204

Deleted.

post/v1/workspaces/{workspace_id}/webhooks/endpoints/{endpoint_id}/rotate-secretAuth

Rotate the signing secret. The new one is returned once.

For `grace_hours` (default 24) the previous secret keeps signing too: `webhook-signature` then lists one `v1,` signature per secret, so receivers can switch over without dropping a delivery.

Path parameters

workspace_id*string
endpoint_id*string · uuid

Headers

Idempotency-Keystring

Opaque client-generated string (1-256 chars, `[A-Za-z0-9_-:]`) that makes a retry of this request safe for 24 hours. The same key with a different body returns 409 `IDEMPOTENCY_KEY_REUSE`.

Request body

grace_hoursnumber

Hours the previous secret keeps signing too (0 ends it now).

Response · 200

id*string · uuid
scope_type*string

What owns the endpoint: `workspace`, `app` or `community`.

Example: "workspace"

scope_id*string

Id of the owning workspace, app or community.

url*string · uri

Example: "https://api.acme.com/webhooks"

description*string

Example: "Production receiver"

event_types*array

Subscribed event types. Empty means every event, including types added later.

Example: []

enabled*boolean

The on/off switch (`PATCH … { enabled }`).

status*enum (3)

`enabled` receives events. `disabled` was switched off. `auto_disabled` was switched off after 20 failed deliveries in a row or 24 hours of failures; `PATCH { enabled: true }` turns it back on.

disabled_at*string · date-time
disabled_reason*enum (2)
consecutive_failures*number

Failed automatic deliveries in a row; a success resets it.

first_failure_at*string · date-time

Start of the current failure streak.

signing_secret_hint*string

Last 4 characters of the signing secret.

Example: "x9Q="

prev_signing_secret_active*boolean

True while the previous secret still signs too (after a rotation, for its grace period).

prev_signing_secret_expires_at*string · date-time

Until then the previous secret signs too (after a rotation).

legacy_signature*boolean

True for endpoints carried over from the product's previous webhook system: they also receive the old signature header until its sunset date.

created_at*string · date-time
updated_at*string · date-time
signing_secret*string

The signing secret, shown once: pass it unchanged to any Standard Webhooks verifier. Store it now; it cannot be shown again.

Example: "whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw"

Example

Request

POST /v1/workspaces/{workspace_id}/webhooks/endpoints/{endpoint_id}/rotate-secret
Authorization: Bearer YOUR_TOKEN
Content-Type: application/json

{
  "grace_hours": 0
}

Response

{
  "id": "00000000-0000-0000-0000-000000000000",
  "scope_type": "workspace",
  "scope_id": "string",
  "url": "https://api.acme.com/webhooks",
  "description": "Production receiver",
  "event_types": [],
  "enabled": false,
  "status": "enabled",
  "disabled_at": "2026-01-01T00:00:00.000Z",
  "disabled_reason": "manual",
  "consecutive_failures": 0,
  "first_failure_at": "2026-01-01T00:00:00.000Z",
  "signing_secret_hint": "x9Q=",
  "prev_signing_secret_active": false,
  "prev_signing_secret_expires_at": "2026-01-01T00:00:00.000Z",
  "legacy_signature": false,
  "created_at": "2026-01-01T00:00:00.000Z",
  "updated_at": "2026-01-01T00:00:00.000Z",
  "signing_secret": "whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw"
}
post/v1/workspaces/{workspace_id}/webhooks/endpoints/{endpoint_id}/testAuth

Send a test event to this endpoint now and return the result.

Delivers the catalogue example of `event_type`, signed like any delivery, with `"test": true` in the body. Works while the endpoint is switched off or not subscribed to that type, is logged in the event log, and is never retried or counted toward an automatic disable. Waits up to 15 s for your receiver.

Path parameters

workspace_id*string
endpoint_id*string · uuid

Headers

Idempotency-Keystring

Opaque client-generated string (1-256 chars, `[A-Za-z0-9_-:]`) that makes a retry of this request safe for 24 hours. The same key with a different body returns 409 `IDEMPOTENCY_KEY_REUSE`.

Request body

event_type*enum (3)

Which event to send; its catalogue example is the `data`.

Example: "entry.created"

Response · 200 The attempt, successful or not.

id*string · uuid
event_id*string

Example: "evt_3f0c3c1e-8a2b-4c55-9d3f-0a1b2c3d4e5f"

endpoint_id*string · uuid
event_type*string

Example: "user.created"

attempt_number*number

1 for the first delivery; retries and replays count up.

trigger*enum (3)

`automatic` (first delivery or a retry), `replay` or `test`.

url*string · uri

Where it was sent.

status_code*number

HTTP status; null when no response came back.

succeeded*boolean

True for a 2xx response.

error_message*string

Example: "request timed out"

response_body*string

The first 1 KiB of the response body.

latency_ms*number
attempted_at*string · date-time

Example

Request

POST /v1/workspaces/{workspace_id}/webhooks/endpoints/{endpoint_id}/test
Authorization: Bearer YOUR_TOKEN
Content-Type: application/json

{
  "event_type": "entry.created"
}

Response

{
  "id": "00000000-0000-0000-0000-000000000000",
  "event_id": "evt_3f0c3c1e-8a2b-4c55-9d3f-0a1b2c3d4e5f",
  "endpoint_id": "00000000-0000-0000-0000-000000000000",
  "event_type": "user.created",
  "attempt_number": 0,
  "trigger": "automatic",
  "url": "string",
  "status_code": 0,
  "succeeded": false,
  "error_message": "request timed out",
  "response_body": "string",
  "latency_ms": 0,
  "attempted_at": "2026-01-01T00:00:00.000Z"
}
get/v1/workspaces/{workspace_id}/webhooks/eventsAuth

List webhook events, newest first (kept for 30 days).

Every event logged while at least one endpoint existed, with the latest state per endpoint and when the next retry is due.

Path parameters

workspace_id*string

Query parameters

typearray

Only these types. Repeat the parameter or comma-separate.

endpoint_idstring · uuid

Only events sent (or queued) to this endpoint.

statusenum (4)
created_afterstring · date-time

Only events created at or after this instant.

created_beforestring · date-time

Only events created before this instant.

limitnumber
cursorstring

The previous page's `pagination.next_cursor`.

Response · 200

data*array
pagination*object
next_cursor*string

Pass as `cursor` to get the next page; null on the last page.

has_more*boolean

Whether another page follows.

Example

Request

GET /v1/workspaces/{workspace_id}/webhooks/events
Authorization: Bearer YOUR_TOKEN

Response

{
  "data": [
    {
      "id": "evt_3f0c3c1e-8a2b-4c55-9d3f-0a1b2c3d4e5f",
      "type": "entry.created",
      "created_at": "2026-01-01T00:00:00.000Z",
      "test": false,
      "status": "pending",
      "payload": {},
      "deliveries": [
        {
          "endpoint_id": "00000000-0000-0000-0000-000000000000",
          "attempt_number": 0,
          "status_code": 0,
          "succeeded": false,
          "attempted_at": "2026-01-01T00:00:00.000Z",
          "next_attempt_at": "2026-01-01T00:00:00.000Z"
        }
      ]
    }
  ],
  "pagination": {
    "next_cursor": "string",
    "has_more": false
  }
}
get/v1/workspaces/{workspace_id}/webhooks/events/{event_id}Auth

Get one webhook event with every delivery attempt.

Path parameters

workspace_id*string
event_id*string

The event id, `evt_<uuid>`.

Response · 200

id*string

Sent as `webhook-id`; the same on every retry and replay.

Example: "evt_3f0c3c1e-8a2b-4c55-9d3f-0a1b2c3d4e5f"

type*enum (3)

Example: "entry.created"

created_at*string · date-time
test*boolean

Made by "Send test".

status*enum (4)

`pending`: a delivery is queued. `failed`: the latest attempt to some endpoint failed and none is queued. `delivered`: every endpoint answered 2xx. `not_sent`: no endpoint subscribed.

payload*object

The JSON body exactly as sent.

deliveries*array

Latest attempt per endpoint (an endpoint whose first attempt is still queued is not listed yet).

attempts*array

Every attempt, newest first.

Example

Request

GET /v1/workspaces/{workspace_id}/webhooks/events/{event_id}
Authorization: Bearer YOUR_TOKEN

Response

{
  "id": "evt_3f0c3c1e-8a2b-4c55-9d3f-0a1b2c3d4e5f",
  "type": "entry.created",
  "created_at": "2026-01-01T00:00:00.000Z",
  "test": false,
  "status": "pending",
  "payload": {},
  "deliveries": [
    {
      "endpoint_id": "00000000-0000-0000-0000-000000000000",
      "attempt_number": 0,
      "status_code": 0,
      "succeeded": false,
      "attempted_at": "2026-01-01T00:00:00.000Z",
      "next_attempt_at": "2026-01-01T00:00:00.000Z"
    }
  ],
  "attempts": [
    {
      "id": "00000000-0000-0000-0000-000000000000",
      "event_id": "evt_3f0c3c1e-8a2b-4c55-9d3f-0a1b2c3d4e5f",
      "endpoint_id": "00000000-0000-0000-0000-000000000000",
      "event_type": "user.created",
      "attempt_number": 0,
      "trigger": "automatic",
      "url": "string",
      "status_code": 0,
      "succeeded": false,
      "error_message": "request timed out",
      "response_body": "string",
      "latency_ms": 0,
      "attempted_at": "2026-01-01T00:00:00.000Z"
    }
  ]
}
post/v1/workspaces/{workspace_id}/webhooks/events/{event_id}/replayAuth

Send a logged event again, to its endpoints or to one endpoint.

Same body and `webhook-id`, freshly signed, to every switched-on endpoint that receives this event type now, or only to `endpoint_id`. Waits for the receivers (up to 15 s each) and returns the attempts. A success cancels a retry still queued for that endpoint.

Path parameters

workspace_id*string
event_id*string

The event id, `evt_<uuid>`.

Query parameters

endpoint_idstring · uuid

Send to this endpoint only.

Headers

Idempotency-Keystring

Opaque client-generated string (1-256 chars, `[A-Za-z0-9_-:]`) that makes a retry of this request safe for 24 hours. The same key with a different body returns 409 `IDEMPOTENCY_KEY_REUSE`.

Response · 200

event_id*string
attempts*array

One attempt per endpoint it was sent to.

Example

Request

POST /v1/workspaces/{workspace_id}/webhooks/events/{event_id}/replay
Authorization: Bearer YOUR_TOKEN

Response

{
  "event_id": "string",
  "attempts": [
    {
      "id": "00000000-0000-0000-0000-000000000000",
      "event_id": "evt_3f0c3c1e-8a2b-4c55-9d3f-0a1b2c3d4e5f",
      "endpoint_id": "00000000-0000-0000-0000-000000000000",
      "event_type": "user.created",
      "attempt_number": 0,
      "trigger": "automatic",
      "url": "string",
      "status_code": 0,
      "succeeded": false,
      "error_message": "request timed out",
      "response_body": "string",
      "latency_ms": 0,
      "attempted_at": "2026-01-01T00:00:00.000Z"
    }
  ]
}
get/v1/workspaces/{workspace_id}/webhooksAuth

List webhooks for the workspace, newest first, cursor-paginated (secrets masked)

Deprecated: use `/v1/workspaces/{workspace_id}/webhooks/endpoints` (event descriptions, Send test that reports the result, event log, replay, rotation with a grace period). Responses carry `Deprecation: true` and `Sunset: Fri, 15 Jan 2027 00:00:00 GMT`.

Path parameters

workspace_id*string

Query parameters

limitinteger

Page size. Defaults to 20; values above 100 are clamped to 100.

cursorstring

Opaque cursor from the previous page's `pagination.next_cursor`. Omit for the first page.

Response · 200 Page of webhooks for the workspace.

data*array
pagination*object
next_cursor*string

Opaque cursor for the next page. `null` when no more pages are available.

Example: "eyJwb3NpdGlvbiI6MjB9"

has_more*boolean

True iff another page exists; if true, pass `next_cursor` on the next request.

Example: false

Example

Request

GET /v1/workspaces/{workspace_id}/webhooks
Authorization: Bearer YOUR_TOKEN

Response

{
  "data": [
    {
      "id": "string",
      "workspace_id": "string",
      "url": "string",
      "events": [
        "entry.created"
      ],
      "active": false,
      "failure_count": 0,
      "last_status_code": 0,
      "last_attempt_at": "string",
      "auto_disabled_at": "string",
      "first_failure_at": "string",
      "created_at": "string",
      "secret_set": false
    }
  ],
  "pagination": {
    "next_cursor": "eyJwb3NpdGlvbiI6MjB9",
    "has_more": false
  }
}
post/v1/workspaces/{workspace_id}/webhooksAuth

Create a workspace webhook. The response includes the signing secret in clear EXACTLY ONCE — copy it now.

Deprecated: use `/v1/workspaces/{workspace_id}/webhooks/endpoints` (event descriptions, Send test that reports the result, event log, replay, rotation with a grace period). Responses carry `Deprecation: true` and `Sunset: Fri, 15 Jan 2027 00:00:00 GMT`.

Path parameters

workspace_id*string

Headers

Idempotency-Keystring

Stripe-style idempotency key. The signing secret is returned exactly once; a retry without this header creates a second webhook + secret you never see the plaintext for. 24h TTL.

Request body

url*string

Destination URL (must be https in prod)

events*array

Events to subscribe to

Example: ["entry.created","entry.approved"]

activeboolean

Disabled webhooks store events but never deliver

Response · 201 Webhook created. Includes plaintext `signing_secret` exactly once.

id*string
workspace_id*string
url*string
events*array

Subscribed events (`event_types` on the new routes).

active*boolean

Whether it receives events (`enabled` on the new routes).

failure_count*number

Failed deliveries in a row (`consecutive_failures` on the new routes).

last_status_code*number

HTTP status of the latest attempt.

last_attempt_at*string

When the latest attempt was made.

auto_disabled_at*string

Set when it was switched off automatically after sustained failures.

first_failure_at*string
created_at*string
secret_setboolean

Always true: every webhook has a signing secret.

signing_secret*string

Plaintext signing secret (`whsec_…`), returned EXACTLY ONCE. Persist immediately; it does not appear on later reads.

secret_one_time_reveal*string

The same secret as `signing_secret`, under the name older clients read.

Example

Request

POST /v1/workspaces/{workspace_id}/webhooks
Authorization: Bearer YOUR_TOKEN
Content-Type: application/json

{
  "url": "string",
  "events": [
    "entry.created",
    "entry.approved"
  ],
  "active": false
}

Response

{
  "id": "string",
  "workspace_id": "string",
  "url": "string",
  "events": [
    "entry.created"
  ],
  "active": false,
  "failure_count": 0,
  "last_status_code": 0,
  "last_attempt_at": "string",
  "auto_disabled_at": "string",
  "first_failure_at": "string",
  "created_at": "string",
  "secret_set": false,
  "signing_secret": "string",
  "secret_one_time_reveal": "string"
}
get/v1/workspaces/{workspace_id}/webhooks/{id}Auth

Get a webhook (secret masked)

Deprecated: use `/v1/workspaces/{workspace_id}/webhooks/endpoints` (event descriptions, Send test that reports the result, event log, replay, rotation with a grace period). Responses carry `Deprecation: true` and `Sunset: Fri, 15 Jan 2027 00:00:00 GMT`.

Path parameters

workspace_id*string
id*string

Response · 200 Webhook.

id*string
workspace_id*string
url*string
events*array

Subscribed events (`event_types` on the new routes).

active*boolean

Whether it receives events (`enabled` on the new routes).

failure_count*number

Failed deliveries in a row (`consecutive_failures` on the new routes).

last_status_code*number

HTTP status of the latest attempt.

last_attempt_at*string

When the latest attempt was made.

auto_disabled_at*string

Set when it was switched off automatically after sustained failures.

first_failure_at*string
created_at*string
secret_setboolean

Always true: every webhook has a signing secret.

Example

Request

GET /v1/workspaces/{workspace_id}/webhooks/{id}
Authorization: Bearer YOUR_TOKEN

Response

{
  "id": "string",
  "workspace_id": "string",
  "url": "string",
  "events": [
    "entry.created"
  ],
  "active": false,
  "failure_count": 0,
  "last_status_code": 0,
  "last_attempt_at": "string",
  "auto_disabled_at": "string",
  "first_failure_at": "string",
  "created_at": "string",
  "secret_set": false
}
patch/v1/workspaces/{workspace_id}/webhooks/{id}Auth

Update url / events / active flag. `active: true` also clears an automatic switch-off.

Deprecated: use `/v1/workspaces/{workspace_id}/webhooks/endpoints` (event descriptions, Send test that reports the result, event log, replay, rotation with a grace period). Responses carry `Deprecation: true` and `Sunset: Fri, 15 Jan 2027 00:00:00 GMT`.

Path parameters

workspace_id*string
id*string

Request body

urlstring

New URL

eventsarray

New event subscription

activeboolean

Pause/resume the webhook

Response · 200 Updated webhook (secret masked).

id*string
workspace_id*string
url*string
events*array

Subscribed events (`event_types` on the new routes).

active*boolean

Whether it receives events (`enabled` on the new routes).

failure_count*number

Failed deliveries in a row (`consecutive_failures` on the new routes).

last_status_code*number

HTTP status of the latest attempt.

last_attempt_at*string

When the latest attempt was made.

auto_disabled_at*string

Set when it was switched off automatically after sustained failures.

first_failure_at*string
created_at*string
secret_setboolean

Always true: every webhook has a signing secret.

Example

Request

PATCH /v1/workspaces/{workspace_id}/webhooks/{id}
Authorization: Bearer YOUR_TOKEN
Content-Type: application/json

{
  "url": "string",
  "events": [
    "entry.created"
  ],
  "active": false
}

Response

{
  "id": "string",
  "workspace_id": "string",
  "url": "string",
  "events": [
    "entry.created"
  ],
  "active": false,
  "failure_count": 0,
  "last_status_code": 0,
  "last_attempt_at": "string",
  "auto_disabled_at": "string",
  "first_failure_at": "string",
  "created_at": "string",
  "secret_set": false
}
delete/v1/workspaces/{workspace_id}/webhooks/{id}Auth

Delete a webhook

Deprecated: use `/v1/workspaces/{workspace_id}/webhooks/endpoints` (event descriptions, Send test that reports the result, event log, replay, rotation with a grace period). Responses carry `Deprecation: true` and `Sunset: Fri, 15 Jan 2027 00:00:00 GMT`.

Path parameters

workspace_id*string
id*string

Response · 204

Deleted.

get/v1/workspaces/{workspace_id}/webhooks/{id}/deliveriesAuth

Delivery attempts for a webhook, newest first, cursor-paginated (default 50, max 200)

Deprecated: use `/v1/workspaces/{workspace_id}/webhooks/endpoints` (event descriptions, Send test that reports the result, event log, replay, rotation with a grace period). Responses carry `Deprecation: true` and `Sunset: Fri, 15 Jan 2027 00:00:00 GMT`.

Path parameters

workspace_id*string
id*string

Query parameters

limitinteger

Page size. Defaults to 50; values above 200 are clamped to 200.

cursorstring

Opaque cursor from the previous page's `pagination.next_cursor`. Omit for the first page.

statusenum (2)

`success` is a 2xx response; `failed` is everything else, including attempts that got no response at all (null `status_code`). Unrecognised values are rejected with a 400.

Response · 200 Delivery attempts, newest first.

data*array
pagination*object
next_cursor*string

Opaque cursor for the next page. `null` when no more pages are available.

Example: "eyJwb3NpdGlvbiI6MjB9"

has_more*boolean

True iff another page exists; if true, pass `next_cursor` on the next request.

Example: false

Example

Request

GET /v1/workspaces/{workspace_id}/webhooks/{id}/deliveries
Authorization: Bearer YOUR_TOKEN

Response

{
  "data": [
    {
      "id": "00000000-0000-0000-0000-000000000000",
      "webhook_id": "00000000-0000-0000-0000-000000000000",
      "event_id": "string",
      "event_type": "entry.created",
      "attempt_number": 0,
      "status_code": 0,
      "response_body_truncated": "string",
      "error_message": "string",
      "latency_ms": 0,
      "attempted_at": "2026-01-01T00:00:00.000Z"
    }
  ],
  "pagination": {
    "next_cursor": "eyJwb3NpdGlvbiI6MjB9",
    "has_more": false
  }
}
post/v1/workspaces/{workspace_id}/webhooks/{id}/rotate-secretAuth

Rotate the signing secret. Returns the new secret in clear EXACTLY ONCE; the old one stops at once.

Deprecated: use `/v1/workspaces/{workspace_id}/webhooks/endpoints` (event descriptions, Send test that reports the result, event log, replay, rotation with a grace period). Responses carry `Deprecation: true` and `Sunset: Fri, 15 Jan 2027 00:00:00 GMT`.

Path parameters

workspace_id*string
id*string

Response · 201 Secret rotated. Includes the new plaintext secret exactly once.

id*string · uuid
secret*string

New plaintext signing secret, returned EXACTLY ONCE. Persist immediately — it does not appear on subsequent GETs.

Example

Request

POST /v1/workspaces/{workspace_id}/webhooks/{id}/rotate-secret
Authorization: Bearer YOUR_TOKEN

Response

{
  "id": "00000000-0000-0000-0000-000000000000",
  "secret": "string"
}
post/v1/workspaces/{workspace_id}/webhooks/{id}/testAuth

Send the example of an event to this webhook; the attempt shows in `/deliveries`

Deprecated: use `/v1/workspaces/{workspace_id}/webhooks/endpoints` (event descriptions, Send test that reports the result, event log, replay, rotation with a grace period). Responses carry `Deprecation: true` and `Sunset: Fri, 15 Jan 2027 00:00:00 GMT`.

Path parameters

workspace_id*string
id*string

Request body

event*enum (3)

Event type to fire as a synthetic test

waitlist_idstring

Accepted for older clients and ignored: the test delivers the event's example payload from `GET …/webhooks/event-types`.

Response · 202

Sent. Read the outcome from `/deliveries`.

Example

Request

POST /v1/workspaces/{workspace_id}/webhooks/{id}/test
Authorization: Bearer YOUR_TOKEN
Content-Type: application/json

{
  "event": "entry.created",
  "waitlist_id": "string"
}

Stats

get/v1/workspaces/{workspace_id}/statsAuth

Waitlist stats for a workspace

Waitlist count, total entries, signups in the window, the most recent signup and a daily signup series, across every waitlist in the workspace. **Active** means a signup: a new waitlist entry. Days are UTC. Counts only: no entrant emails, names, referrers or IPs. Cached for up to 60 seconds (`generated_at`).

Path parameters

workspace_id*string · uuid

Workspace id.

Query parameters

windowenum (3)

Window for `in_window.signups` and the length of `series`, in UTC days ending today.

Response · 200 Waitlist stats.

window*enum (3)

Window the counts cover.

Example: "30d"

generated_at*string · date-time

When these numbers were computed. Responses are cached for up to 60 seconds.

Example: "2026-10-07T09:00:00.000Z"

last_activity_at*string · date-time

Most recent signup on any of the workspace's waitlists. Null when there has never been one.

Example: "2026-10-07T08:41:12.000Z"

totals*object

All-time counts.

in_window*object

Counts for the window.

series*object

Daily signups.

Example

Request

GET /v1/workspaces/{workspace_id}/stats
Authorization: Bearer YOUR_TOKEN

Response

{
  "window": "30d",
  "generated_at": "2026-10-07T09:00:00.000Z",
  "last_activity_at": "2026-10-07T08:41:12.000Z",
  "totals": {},
  "in_window": {},
  "series": {}
}