Changelog

Updates, improvements, and new features in Enlist.

Revised field limits

A string value in metadata or in a custom fields value may now be 1000 characters, up from 500. Nothing else about either shape changed: metadata is still capped at 10 keys and still stamped once at creation, custom fields are still typed and still validated against the waitlist's definitions.

The old limit was set for short values — a company name, a plan tier — and a free-text note is the thing people actually kept hitting it with. 500 characters is about a paragraph; a "what are you hoping to use this for?" answer runs past it more often than not, and the request came back a 400 with the answer already typed.

The two limits are now one number rather than two that happened to agree. metadata is immutable after creation and custom fields are not, so a value in the wrong one gets moved; discovering a different length limit on the other side of that move was a second surprise on top of the first.

Field values may now contain newlines too. They were rejected at the API's front door to keep a line break out of an email subject, which charged every value — including the ones no template interpolates — for a rule about a single line of output. The email body already rendered them correctly: a line break becomes a <br />, a blank line starts a new paragraph. Only the subject cannot span lines, and that is now handled where it happens, by collapsing newlines to spaces as the subject is rendered.

That split matters because the two halves of a subject line come from two different people. The account owner writes the template and chooses whether {{note}} belongs in the subject; whoever fills in the signup form supplies the text. No amount of care on the owner's part stops someone pasting two lines into a free-text field, so the failure landed on the person who could not prevent it.

One limit went the other way: a waitlist may now define at most 20 custom fields. There was no cap before, and with none, the size of a signup request had no bound at all — 20 fields at 1000 characters each is the ceiling that number now sets, and those values are carried by every list response and every CSV export thereafter. Twenty is well past any real signup form; a form longer than that is an application, and an application wants a form builder rather than a waitlist.

It is a flat limit, not a plan one. The quotas in your plan meter capacity, which a delete frees up again; a field definition is structure. An account moving to a smaller plan with a dozen fields already defined would be over a plan-varying limit with nothing sensible to do about it, and nobody upgrades a waitlist to buy form fields.

Raising a limit accepts everything it accepted before, so the two length changes need no migration and no version pin. If you are validating lengths on your own side before calling us, those are the numbers to change. The field cap only applies when creating a new definition — no existing waitlist is affected, and if you somehow have more than 20 fields today they all keep working.

Deleting is its own permission

Deleting now needs a scope of its own. waitlists:write and signups:write used to carry removal along with creation, which meant the most common key in the product — a server handling a signup form — could erase any signup on the account despite never having a reason to.

What changed:

  • Two new scopes: waitlists:delete for DELETE /v1/waitlists/:id, and signups:delete for DELETE /v1/waitlists/:id/signups/:signupId. The write scopes no longer grant either.
  • Scopes are now fixed at the moment a key is minted. Checking every box grants everything that exists then, and nothing added later. Previously it stored *, which meant a key issued last month silently gained any permission invented since.
  • Keys created before this change keep exactly the access they had — the wildcard was expanded into the five scopes that existed at the time, so none of them picked up the two new ones. Existing integrations are unaffected unless they delete, in which case mint a new key with the delete scope.
  • Waitlists can now be renamed and deleted through the API, the SDK and the dashboard. Renaming changes an internal label only: ids, positions and referral codes all survive.

Deleting a waitlist takes every signup on it, permanently. Export first — the dashboard offers it inside the confirmation dialog, and the API has GET /v1/waitlists/:id/signups/export.

Custom fields

Waitlists now support strongly-typed field definitions. Define the fields you want to collect at signup time — company name, team size, plan tier, anything — and they are validated automatically on every signup, stored on the row, and available as {{key}} placeholders in the welcome email template.

How it works:

  1. Define fields on a waitlist via the API (POST /v1/waitlists/:id/fields). Each field has a key, label, type (string, number, or boolean), and an optional required flag.
  2. Pass values in the fields object when adding a signup. Required fields that are missing return 400. Unknown keys are stripped silently.
  3. Values appear in the signup response and are stored permanently on the signup row. Use {{company}} (or whatever key you defined) in your welcome email template.

API surface:

MethodPathReturns
GET/v1/waitlists/:id/fieldsAll field definitions
POST/v1/waitlists/:id/fieldsCreated field definition
PATCH/v1/waitlists/:id/fields/:keyUpdated field definition
DELETE/v1/waitlists/:id/fields/:key204

key is immutable after creation. label, required, and position can be updated. Deleting a definition does not touch existing signup values.

SDK: waitlists.fields.list(), .create(), .update(), .delete() — all on @enlistdev/sdk@0.3.0.

MCP: list_waitlist_fields tool added. add_signup now accepts fields. Both on @enlistdev/mcp@0.3.0.

Constraints: field keys must be lowercase alphanumeric + underscores (e.g. company, headcount) and cannot shadow built-in template variables. Values are checked against the declared type, and there are limits on how long a value may be and how many fields a waitlist may define — see the API reference for the current numbers.

Referrals

Every signup now gets a referral code, and Enlist tracks who referred whom. You decide what a referral is worth — Enlist just keeps the count honest.

What's included:

  • Signup response: POST /v1/waitlists/:id/signups returns referral_code, ten URL-safe characters. Put it behind ?ref= and you have a share link.
  • Signup request: pass someone else's code as referral_code and their referral_count goes up, inside the same transaction that assigns the position.
  • Signup shape: referral_code, referral_count, and referred_by are on every signup returned by the API and the SDK.
  • MCP: add_signup takes an optional referralCode, so an agent wiring up a form can pass the ref param straight through.

Positions never change. A referral does not move anyone up the list — it increments a counter you can sort by, threshold on, or subtract from the number you display. That keeps position meaning exactly one thing: the order people joined in.

An unknown, malformed, wrong-waitlist, or self-referral code is ignored rather than rejected, so a mangled share link never costs you the signup.

The SDK and MCP server bump to 0.3.0 with this release.

Account deletion

You can now permanently delete your account from the Settings page.

Clicking "Delete account" opens a confirmation dialog that requires you to type your email address before the button enables. On confirm, all data associated with your account is removed — waitlists, signups, API keys, email configuration, and webhooks — and you are signed out immediately.

There is no grace period and no undo. The confirmation step exists to prevent accidents.

CSV export

You can now export your entire waitlist as a CSV — one click from the dashboard, or a single API call.

What's included:

  • Dashboard: an Export CSV button on the waitlist page downloads the file directly via your browser session. No API key needed in the browser.
  • API: GET /v1/waitlists/:id/signups/export streams the full signup list as CSV. Useful for piping into scripts or external tools.
  • SDK: waitlists.exportSignups(id) returns the CSV as a string.

The export streams in batches so it won't fall over on large lists. Values that could trigger formula execution in Excel or Google Sheets (starting with =, +, -, @) are sanitised automatically.

The SDK bumps to 0.2.0 with this release.

Launch

Enlist is live. Waitlist infrastructure as an API — positions assigned server-side, delivery state included.

What's available today:

  • REST API with full OpenAPI 3.1 spec and Swagger UI at /api/docs
  • TypeScript SDK (@enlistdev/sdk) — complete and tested, npm release pending
  • MCP server at https://api.enlist.dev/mcp — connect via Claude Code, Cursor, or any MCP client, no install required
  • UTM source and referrer tracking per signup
  • Custom metadata (up to 10 key/value pairs per signup)
  • Dashboard with signup analytics and daily series

Free tier: 1 waitlist, no credit card required. See pricing for current limits.

Coming soon: Email (welcome emails + delivery tracking), webhooks, referral tracking, and paid tiers.

Scoped API keys

API keys now carry explicit permission scopes. When you create a key you choose exactly what it can do — no more handing out full-access credentials for a read-only integration.

What changed:

  • New keys are created through a two-step dialog: pick a name, then check the permissions you want.
  • The keys table in the dashboard shows a Scopes column: scoped keys list their permissions as badges, unrestricted keys show "Full access".
  • API routes return 403 Forbidden when a key lacks the required scope. The error code is forbidden.

Scoped keys are useful any time you want to give a third-party integration the minimum access it needs — for example, a read-only key for an analytics pipeline or a signup-only key for a landing page.

Webhook delivery log and new events

Webhooks got a significant expansion: four new event types, per-subscription filtering, and a delivery log so you can see exactly what was sent and when.

New event types:

EventFires when
signup.removedA signup is deleted
signup.email_deliveredWelcome email reaches the inbox
signup.email_openedWelcome email is opened
signup.email_clickedA link in the welcome email is clicked

Per-subscription filtering:

Each webhook endpoint now has an event filter. Check only the events you care about — unchecking everything resets to receiving all events (the safe default).

Delivery log:

Every delivery attempt is recorded: event type, HTTP status, and any error. Check the log from the webhook settings page to debug failed deliveries without guessing.

The existing signup.created event and all previously configured webhooks are unaffected.