Skip to content

Reviews, references and referrals: one ask at the right moment

Reviews, references and referrals: one ask at a time, after a delivered outcome, shown only with their OK.

Each Customer gets one ask at a time, a review, a reference or a referral, after a delivered outcome.

One ask, at the right momentMode try
You, to your agent

Ask our happiest customer for a review and a referral. Don’t send.

curl -X POST https://api.sb/leaseline.app/metrics/query \
  -H "Authorization: Bearer $STARTUP_BUILDER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"metric":"customer-health","shape":"trend","breakdown":"customer"}'

curl -X POST "https://api.sb/leaseline.app/reviews/review_14/publish?mode=try" \
  -H "Authorization: Bearer $STARTUP_BUILDER_API_KEY"

curl -X POST "https://api.sb/leaseline.app/messages/message_95/schedule?mode=try" \
  -H "Authorization: Bearer $STARTUP_BUILDER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"at":"2026-10-21T15:00:00Z"}'

curl -X POST https://api.sb/leaseline.app/metrics/query \
  -H "Authorization: Bearer $STARTUP_BUILDER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"metric":"referral-share","shape":"trend","window":"28d"}'

# Not live yet (gaps #3)
curl -X POST "https://api.sb/leaseline.app/referrals/invite?mode=try" \
  -H "Authorization: Bearer $STARTUP_BUILDER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"customer":"customer_31"}'
import { $ } from 'startup-builder'

const { rows: health } = await $.Metric.query({ metric: 'customer-health', shape: 'trend', breakdown: 'customer' })   // Elm Street Rentals: health 92, activated since July, no open ask
await $.Review.publish({ review: 'review_14' }, { mode: 'try' })   // Maple Court’s 5-star review, on your site
// 422 · Maple Court’s review did not publish: they have not said yes to showing it.
// display-consent: Ask once for their OK to show it; it publishes the moment they say yes. · then Message.schedule
await $.Message.schedule({ message: 'message_95', at: '2026-10-21T15:00:00Z' }, { mode: 'try' })   // the referral ask to Elm Street, today: one Message
const { rows } = await $.Metric.query({ metric: 'referral-share', shape: 'trend', window: '28d' })
await $.Referral.invite({ customer: 'customer_31' }, { mode: 'try' })   // Not live yet, gaps #3
return rows
npx startup-builder do Metric.query --json '{"metric":"customer-health","shape":"trend","breakdown":"customer"}'
npx startup-builder do Review.publish --json '{"review":"review_14"}' --try
npx startup-builder do Message.schedule --json '{"message":"message_95","at":"2026-10-21T15:00:00Z"}' --try
npx startup-builder do Metric.query --json '{"metric":"referral-share","shape":"trend","window":"28d"}'
npx startup-builder do Referral.invite --json '{"customer":"customer_31"}' --try   # Not live yet (gaps #3)
{
  "name": "do",
  "arguments": {
    "code": "const { rows: health } = await $.Metric.query({ metric: 'customer-health', shape: 'trend', breakdown: 'customer' })   // Elm Street Rentals: health 92, activated since July, no open ask\nawait $.Review.publish({ review: 'review_14' }, { mode: 'try' })   // Maple Court’s 5-star review, on your site\n// 422 · Maple Court’s review did not publish: they have not said yes to showing it.\n// display-consent: Ask once for their OK to show it; it publishes the moment they say yes. · then Message.schedule\nawait $.Message.schedule({ message: 'message_95', at: '2026-10-21T15:00:00Z' }, { mode: 'try' })   // the referral ask to Elm Street, today: one Message\nconst { rows } = await $.Metric.query({ metric: 'referral-share', shape: 'trend', window: '28d' })\nawait $.Referral.invite({ customer: 'customer_31' }, { mode: 'try' })   // Not live yet, gaps #3\nreturn rows",
    "mode": "try"
  }
}
Response200application/json
[
  {
    "window": "28d",
    "value": 0.12,
    "referred": 2,
    "newCustomers": 17
  }
]
sample · 1 Not live yet

2 of 17 new Customers referred in 28 days · 12 % · ■□□□ Too early to tell · n 17

Metric.query({"metric":"referral-share","shape":"trend","window":"28d"}) · sample · leaseline.app at 2026-10-19

What it does

What it doesPrice per unitMovesWho does itToday
POST /{namespace}/reviews/{review}/publish shows a Review only with the Customer’s OK; a referral ask is one POST /{namespace}/messages/{message}/schedule until Referral.invite (Not live yet, gaps #3) lands.$0.04 per ask (Worker cost)Referred Customers, Sent by your other StartupsPartnerships4 of 5 calls live

Set it up

Check (dry run on a 0-Customer Startup, gaps #8)FixThenYour timeWait
use-when (Not live yet, gaps #8) · pass: 29 activated Customers, 11 with a delivered outcome this monthRun the Play Not yet when names, or wait until the volume is there.POST /{namespace}/metrics/query1 minnone
domain-authenticated · pass: leaseline.appAdd the 3 DNS records shown; it passes once they resolve, often within an hour.POST /{namespace}/domains/{domain}/authenticate0 minnone
role-unstaffed (Not live yet, gaps #8) · warn: Partnerships has no person: asks come to you to approveStaff Partnerships, or leave it: each ask comes to you.POST /{namespace}/roles/{role}/staff0 minnone

The records

Maple Court’s review, held for their OK
{
  "$id": "https://api.sb/leaseline.app/reviews/review_14",
  "$type": "Review",
  "rating": 5,
  "body": "Renewal notices go out on time now.",
  "venue": "site",
  "displayConsent": false,
  "status": "withheld",
  "customer": "customer_14"
}

Calls

CallHereEmitsStatus
POST /{namespace}/metrics/queryElm Street Rentals: health 92, activated since July, no open askMetric.queriedlive
POST /{namespace}/reviews/{review}/publishMaple Court’s 5-star review, on your siteReview.publishedlive
POST /{namespace}/messages/{message}/schedulethe referral ask to Elm Street, today: one MessageMessage.scheduledlive
POST /{namespace}/metrics/queryMetric.queriedlive
Referral.invite (Not live yet, gaps #3)—Not live yet, gaps #3

Events

EventWhen it firesWhat hears it
Order.deliveredan activated Customer, not at risk, gets an outcomestarts one ask, the advocate ladder
Review.publishedthe Customer says yes to showing itthe review on your Pages

Refuses when

422RefusedMaple Court’s review did not publish: they have not said yes to showing it.
CheckFixThen
display-consent failAsk once for their OK to show it; it publishes the moment they say yes.POST /{namespace}/messages/{message}/schedule
JSON
{
  "type": "https://startupbuilder.dev/errors/refused",
  "title": "Refused",
  "status": 422,
  "detail": "Maple Court’s review did not publish: they have not said yes to showing it.",
  "checks": [
    {
      "check": "display-consent",
      "status": "fail",
      "doc": "/code/refusals#check-display-consent",
      "fix": "Ask once for their OK to show it; it publishes the moment they say yes.",
      "then": "Message.schedule"
    }
  ]
}

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

One ask at a timeMode try
You, to your agent

Ask Juniper Homes for a referral too. Don’t send.

curl -X POST "https://api.sb/leaseline.app/messages/message_97/schedule?mode=try" \
  -H "Authorization: Bearer $STARTUP_BUILDER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"at":"2026-10-21T15:00:00Z"}'
import { $ } from 'startup-builder'

return await $.Message.schedule({ message: 'message_97', at: '2026-10-21T15:00:00Z' }, { mode: 'try' })   // the referral ask, while the prepay ask is open
// 422 · The referral ask to Juniper Homes did not send: their prepay ask is open until Nov 3.
// customer-ask-cap: Wait for the open ask to close; Decision.list names the Play that holds them. · then Decision.list
npx startup-builder do Message.schedule --json '{"message":"message_97","at":"2026-10-21T15:00:00Z"}' --try
{
  "name": "do",
  "arguments": {
    "code": "return await $.Message.schedule({ message: 'message_97', at: '2026-10-21T15:00:00Z' }, { mode: 'try' })   // the referral ask, while the prepay ask is open\n// 422 · The referral ask to Juniper Homes did not send: their prepay ask is open until Nov 3.\n// customer-ask-cap: Wait for the open ask to close; Decision.list names the Play that holds them. · then Decision.list",
    "mode": "try"
  }
}
Response422application/json
{
  "type": "https://startupbuilder.dev/errors/refused",
  "title": "Refused",
  "status": 422,
  "detail": "The referral ask to Juniper Homes did not send: their prepay ask is open until Nov 3.",
  "checks": [
    {
      "check": "customer-ask-cap",
      "status": "fail",
      "doc": "/code/refusals#check-customer-ask-cap",
      "fix": "Wait for the open ask to close; Decision.list names the Play that holds them.",
      "then": "Decision.list"
    }
  ]
}
sample
422RefusedThe referral ask to Juniper Homes did not send: their prepay ask is open until Nov 3.
CheckFixThen
customer-ask-cap fail · Not live yet, gaps #3Wait for the open ask to close; Decision.list names the Play that holds them.GET /{namespace}/decisions
JSON
{
  "type": "https://startupbuilder.dev/errors/refused",
  "title": "Refused",
  "status": 422,
  "detail": "The referral ask to Juniper Homes did not send: their prepay ask is open until Nov 3.",
  "checks": [
    {
      "check": "customer-ask-cap",
      "status": "fail",
      "doc": "/code/refusals#check-customer-ask-cap",
      "fix": "Wait for the open ask to close; Decision.list names the Play that holds them.",
      "then": "Decision.list"
    }
  ]
}

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

CheckRefusesFixThen
display-consenta Review shown on your own Pages before its reviewer agreed to displayAsk the reviewer once; the Review shows when they agree.POST /{namespace}/messages/{message}/schedule
customer-ask-cap (Not live yet, gaps #3)a second open ask to one Customer in a window, from any PlayWait for the open ask to close; GET /{namespace}/decisions names the Play that holds them.GET /{namespace}/decisions
keep-first (Not live yet, gaps #8)an upgrade, referral, review or reference ask to a Customer who is at risk, past due or leavingLet their CSM reach them first, then ask again once they are healthy.POST /{namespace}/tasks/next

Plays that use it

PlayWhat it doesUse it when
Review requestsAsks every Customer for an honest Review when their result lands, on the one Venue you need most, and answers every Review within a day.10 or more Customers who reached a delivered outcome, and a Venue listing of your own
Written referencesAnswers a buyer’s reference request within days with a reference an opted-in Customer writes and signs, its credit shown on it.2 or more reference requests a month, and 5 or more Customers in the reference pool
Customer referralsAsks Customers to invite a peer right after they get the result they came for, and rewards both once the peer’s first payment clears refunds.10 or more Customers who reached the outcome you declared, with Stripe connected
Champion followWhen a champion changes jobs, offers them their own setup, pre-loaded at the new company, 30 days in; the old company’s data never moves.30 or more champions on file and 2 or more job moves a quarter
advocate-ladder (Not live yet, gaps #4)The Play that asks a happy Customer for a review, then a reference, then a referral, one ask at a time.today: Order.delivered for an activated Customer starts one ask, a POST /{namespace}/messages/{message}/schedule.

Hub guides it deepens

On startupbuilder.devId
Customerteam:customer

Example

Leaseline, as the fixture ran it
Elm Street Rentals · health 92 · one referral ask Oct 21, a Message today · Maple Court’s review held until they say yes (display-consent) · Juniper’s referral ask refused while their prepay ask is open (customer-ask-cap) · 2 of 17 new Customers referred · sample

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