Skip to content

Sign-up flows: waitlist to trial

A waitlist, an admission order and a trial, each one call, from the first sign-up to the first paid month.

Sign-up flows take a person from the waitlist to an admitted trial and on to the first paid month. Joining costs $0, and each answer to them $0.04 (sample).

Waitlist to trialMode try
You, to your agent

Admit the next 20 people on the Leaseline waitlist and start their 14-day trials. Do not send.

curl -X POST https://api.sb/leaseline.app/waitlists/waitlist_1/join \
  -H "Authorization: Bearer $STARTUP_BUILDER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"email":"sam@kestrel.example"}'

curl -X POST "https://api.sb/leaseline.app/waitlists/waitlist_1/admit?mode=try" \
  -H "Authorization: Bearer $STARTUP_BUILDER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"count":20}'

curl -X POST "https://api.sb/leaseline.app/offers/offer_1/trial?mode=try" \
  -H "Authorization: Bearer $STARTUP_BUILDER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"customer":"customer_52","days":14}'
import { $ } from 'startup-builder'

await $.Waitlist.join({ waitlist: 'waitlist_1', email: 'sam@kestrel.example' })   // a signup from the pricing Page
await $.Waitlist.admit({ waitlist: 'waitlist_1', count: 20 }, { mode: 'try' })   // the next 20, ranked by fit
return await $.Offer.trial({ offer: 'offer_1', customer: 'customer_52', days: 14 }, { mode: 'try' })   // Kestrel Rentals, 40 units
// 422 · Kestrel Rentals’ trial did not start: 14 free days without a card fall below the $29 Offer’s contribution floor.
// contribution-floor: Take a card up front, or shorten the trial to 7 days. · then Offer.trial
npx startup-builder do Waitlist.join --json '{"waitlist":"waitlist_1","email":"sam@kestrel.example"}'
npx startup-builder do Waitlist.admit --json '{"waitlist":"waitlist_1","count":20}' --try
npx startup-builder do Offer.trial --json '{"offer":"offer_1","customer":"customer_52","days":14}' --try
{
  "name": "do",
  "arguments": {
    "code": "await $.Waitlist.join({ waitlist: 'waitlist_1', email: 'sam@kestrel.example' })   // a signup from the pricing Page\nawait $.Waitlist.admit({ waitlist: 'waitlist_1', count: 20 }, { mode: 'try' })   // the next 20, ranked by fit\nreturn await $.Offer.trial({ offer: 'offer_1', customer: 'customer_52', days: 14 }, { mode: 'try' })   // Kestrel Rentals, 40 units\n// 422 · Kestrel Rentals’ trial did not start: 14 free days without a card fall below the $29 Offer’s contribution floor.\n// contribution-floor: Take a card up front, or shorten the trial to 7 days. · then Offer.trial",
    "mode": "try"
  }
}
Response422application/json
{
  "type": "https://startupbuilder.dev/errors/refused",
  "title": "Refused",
  "status": 422,
  "detail": "Kestrel Rentals’ trial did not start: 14 free days without a card fall below the $29 Offer’s contribution floor.",
  "checks": [
    {
      "check": "contribution-floor",
      "status": "fail",
      "observed": "14 days, no card",
      "doc": "/code/refusals#check-contribution-floor",
      "fix": "Take a card up front, or shorten the trial to 7 days.",
      "then": "Offer.trial"
    }
  ]
}
sample

What it does

What you pay forPrice per unit (sample)The call
A signup joined to the waitlist$0POST /{namespace}/waitlists/{waitlist}/join
A batch admitted$0POST /{namespace}/waitlists/{waitlist}/admit
A trial started$0POST /{namespace}/offers/{offer}/trial

Run it

StepCallWhat it does here
1POST /{namespace}/waitlists/{waitlist}/joina signup from the pricing Page
2POST /{namespace}/waitlists/{waitlist}/admitthe next 20, ranked by fit
3POST /{namespace}/offers/{offer}/trialKestrel Rentals, 40 units
422RefusedKestrel Rentals’ trial did not start: 14 free days without a card fall below the $29 Offer’s contribution floor.
CheckFixThen
contribution-floor failTake a card up front, or shorten the trial to 7 days.POST /{namespace}/offers/{offer}/trial
JSON
{
  "type": "https://startupbuilder.dev/errors/refused",
  "title": "Refused",
  "status": 422,
  "detail": "Kestrel Rentals’ trial did not start: 14 free days without a card fall below the $29 Offer’s contribution floor.",
  "checks": [
    {
      "check": "contribution-floor",
      "status": "fail",
      "observed": "14 days, no card",
      "doc": "/code/refusals#check-contribution-floor",
      "fix": "Take a card up front, or shorten the trial to 7 days.",
      "then": "Offer.trial"
    }
  ]
}

It says no, says why, and names the fix.

Set it up

CheckOn the day-0 StartupFixYour timeWaitThen
contribution-floorfailtake a card up front, or shorten the trial to 7 days5 minnonePOST /{namespace}/offers/{offer}/trial
billing-connectedfailconnect Stripe so every trial converts on a live charge10 minnonePOST /{namespace}/integrations/connect
approved-pagespassyour pricing Page states the trial terms0 minnonePOST /{namespace}/sites/{site}/publish

The records

The Waitlist · sample
{
  "waitlist": {
    "$id": "https://api.sb/leaseline.app/waitlists/waitlist_1",
    "$type": "Waitlist",
    "invited": 20,
    "rank": "fit"
  }
}
The Lead the signup wrote · sample
{
  "lead": {
    "$id": "https://api.sb/leaseline.app/leads/lead_52",
    "$type": "Lead",
    "status": "new",
    "interest": "Kestrel Rentals: 40 units",
    "fit": "partial"
  }
}

Calls

CallWhat it doesWhat else happensEvent
POST /{namespace}/waitlists/{waitlist}/joinJoin adds a Lead to a Waitlist for an Offer that is not open to them.—Waitlist.joined
POST /{namespace}/waitlists/{waitlist}/admitAdmit releases Leads from a Waitlist to the Offer they waited for, in the order the Waitlist ranks them.—Waitlist.admitted
POST /{namespace}/offers/{offer}/trialTrial starts a Customer on an Offer free for a set number of days, with or without a card, converting or ending by its terms.With a card on file, converts to a paid Subscription at the end unless canceled first.Offer.trialed

Events

Waitlist.joined · StartupBuilder-Signature
{
  "$id": "https://api.sb/leaseline.app/events/event_1",
  "$type": "Event",
  "name": "Waitlist.joined",
  "at": "2026-10-22T12:00:00Z",
  "seq": 1,
  "data": {
    "lead": {
      "$id": "https://api.sb/leaseline.app/leads/lead_1",
      "$type": "Lead",
      "status": "new"
    },
    "waitlist": {
      "$id": "https://api.sb/leaseline.app/waitlists/waitlist_1",
      "$type": "Waitlist",
      "invited": 1,
      "rank": "signup"
    }
  }
}
Waitlist.admitted · StartupBuilder-Signature
{
  "$id": "https://api.sb/leaseline.app/events/event_1",
  "$type": "Event",
  "name": "Waitlist.admitted",
  "at": "2026-10-22T12:00:00Z",
  "seq": 1,
  "data": {
    "leads": [
      {
        "$id": "https://api.sb/leaseline.app/leads/lead_1",
        "$type": "Lead",
        "status": "new"
      }
    ],
    "waitlist": {
      "$id": "https://api.sb/leaseline.app/waitlists/waitlist_1",
      "$type": "Waitlist",
      "invited": 1,
      "rank": "signup"
    }
  }
}
Offer.trialed · StartupBuilder-Signature
{
  "$id": "https://api.sb/leaseline.app/events/event_1",
  "$type": "Event",
  "name": "Offer.trialed",
  "at": "2026-10-22T12:00:00Z",
  "seq": 1,
  "data": {
    "subscription": {
      "$id": "https://api.sb/leaseline.app/subscriptions/subscription_1",
      "$type": "Subscription",
      "status": "active"
    },
    "offer": {
      "$id": "https://api.sb/leaseline.app/offers/offer_1",
      "$type": "Offer",
      "price": {
        "amount": 29,
        "currency": "USD"
      }
    }
  }
}

Refuses when

CheckRefusesFixThen
contribution-flooran Offer whose Price leaves less than the target margin after the median Customer’s costRaise the Price or lower the cost until the margin clears its target, then publish again.POST /{namespace}/offers/{offer}/publish
billing-connected Not live yet · gaps #8 · today: The dry run renders from the sample; each check names the call that clears it.a Customer-side Play with no live billing recordsConnect Stripe so every charge and Meter reading is live.POST /{namespace}/integrations/connect
no-consenta send or Sequence step whose purpose the Contact has given no Consent forRecord the Contact’s yes, with where they gave it, before the first send.POST /{namespace}/consents
422RefusedKestrel Rentals’ trial did not start: 14 free days without a card fall below the $29 Offer’s contribution floor.
CheckFixThen
contribution-floor failTake a card up front, or shorten the trial to 7 days.POST /{namespace}/offers/{offer}/trial
JSON
{
  "type": "https://startupbuilder.dev/errors/refused",
  "title": "Refused",
  "status": 422,
  "detail": "Kestrel Rentals’ trial did not start: 14 free days without a card fall below the $29 Offer’s contribution floor.",
  "checks": [
    {
      "check": "contribution-floor",
      "status": "fail",
      "observed": "14 days, no card",
      "doc": "/code/refusals#check-contribution-floor",
      "fix": "Take a card up front, or shorten the trial to 7 days.",
      "then": "Offer.trial"
    }
  ]
}

It says no, says why, and names the fix.

422RefusedPinecrest Homes’ second visitor was not enrolled: no Consent on record for sales email.
CheckFixThen
suppressed-contact pass—POST /{namespace}/collections/query
no-consent failRecord the Contact’s yes, with where they gave it, before the first send.POST /{namespace}/consents
JSON
{
  "type": "https://startupbuilder.dev/errors/refused",
  "title": "Refused",
  "status": 422,
  "detail": "Pinecrest Homes’ second visitor was not enrolled: no Consent on record for sales email.",
  "checks": [
    {
      "check": "suppressed-contact",
      "status": "pass"
    },
    {
      "check": "no-consent",
      "status": "fail",
      "observed": "no Consent for purpose sales-email",
      "doc": "/code/refusals#check-no-consent",
      "fix": "Record the Contact’s yes, with where they gave it, before the first send.",
      "then": "Consent.create"
    }
  ]
}

It says no, says why, and names the fix.

Pauses when

StopPauses whenFixThen
kill-switchyou press it, or the injection Stop firesReview what fired, then turn the Process back on.POST /{namespace}/processes/{process}/enable
Campaign.paused · kill-switch · Not live yet, gaps #2 · sample
{
  "$id": "https://api.sb/leaseline.app/events/event_951",
  "$type": "Event",
  "name": "Campaign.paused",
  "at": "2026-10-19T16:45:00Z",
  "data": {
    "campaign": {
      "$id": "https://api.sb/leaseline.app/campaigns/cold-email",
      "$type": "Campaign"
    },
    "reason": {
      "limit": "kill-switch",
      "observed": "pressed by worker_1",
      "bound": "off",
      "window": "now"
    }
  }
}

Plays that use it

PlayWhat it doesIts Metric
Product-led growthGives free Customers enough to reach the result twice, then shows the upgrade inside the task at the Limit, with the real Price.activation-rate
Waitlist interviewsInterviews each Waitlist signup with your disclosed agent, admits by fit under a published cap, and sends a result built on their company.interview-activation-rate
Positioning tournamentTests three ways of saying what you do, as ads that differ in one phrase, and counts only signups that fit and act.qualified-signup-rate
Agent-to-agent sellingLets an agent open a working account in one call, locks its sends until a person claims it, then asks that person once to pay.claimed-paying-rate

Metric it reaches: Activation rate (activation-rate).

Hub guides it deepens

On startupbuilder.devIdWhat it holds
Leadnoun:leadthe record, its fields and every call on it
Analyticscapability:analyticsthe System, its capability and its calls

Example

Leaseline, whenWhat happenedCost (sample)
2026-10-1920 admitted from a waitlist of 64$0
2026-10-19Kestrel Rentals starts a 7-day trial with a card on file$0
day 8Kestrel pays $29: +$347 ARR · n 1 · ■□□□ Too early to tell$0.04 for its one answer

Compiled, not written: edit its page, then regenerate. Every step runs from a sentence or a line of code. In your words: the CRM words you know, and where each lives here.

Contents