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.
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"
}
}application/json[
{
"window": "28d",
"value": 0.12,
"referred": 2,
"newCustomers": 17
}
]
sample · 1 Not live yet2 of 17 new Customers referred in 28 days · 12 % · ■□□□ Too early to tell · n 17
Metric.query({"metric": · sample · leaseline.app at 2026-10-19
What it does
| What it does | Price per unit | Moves | Who does it | Today |
|---|---|---|---|---|
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 Startups | Partnerships | 4 of 5 calls live |
Set it up
| Check (dry run on a 0-Customer Startup, gaps #8) | Fix | Then | Your time | Wait |
|---|---|---|---|---|
use-when (Not live yet, gaps #8) · pass: 29 activated Customers, 11 with a delivered outcome this month | Run the Play Not yet when names, or wait until the volume is there. | POST /{namespace}/metrics/query | 1 min | none |
domain-authenticated · pass: leaseline.app | Add the 3 DNS records shown; it passes once they resolve, often within an hour. | POST /{namespace}/domains/{domain}/authenticate | 0 min | none |
role-unstaffed (Not live yet, gaps #8) · warn: Partnerships has no person: asks come to you to approve | Staff Partnerships, or leave it: each ask comes to you. | POST /{namespace}/roles/{role}/staff | 0 min | none |
The records
{
"$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
| Call | Here | Emits | Status |
|---|---|---|---|
POST /{namespace}/metrics/query | Elm Street Rentals: health 92, activated since July, no open ask | Metric.queried | live |
POST /{namespace}/reviews/{review}/publish | Maple Court’s 5-star review, on your site | Review.published | live |
POST /{namespace}/messages/{message}/schedule | the referral ask to Elm Street, today: one Message | Message.scheduled | live |
POST /{namespace}/metrics/query | Metric.queried | live | |
Referral.invite (Not live yet, gaps #3) | — | Not live yet, gaps #3 |
Events
| Event | When it fires | What hears it |
|---|---|---|
Order.delivered | an activated Customer, not at risk, gets an outcome | starts one ask, the advocate ladder |
Review.published | the Customer says yes to showing it | the review on your Pages |
Refuses when
| Check | Fix | Then |
|---|---|---|
display-consent fail | Ask 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.
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"
}
}application/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| Check | Fix | Then |
|---|---|---|
customer-ask-cap fail · Not live yet, gaps #3 | Wait 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.
| Check | Refuses | Fix | Then |
|---|---|---|---|
display-consent | a Review shown on your own Pages before its reviewer agreed to display | Ask 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 Play | Wait 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 leaving | Let their CSM reach them first, then ask again once they are healthy. | POST /{namespace}/tasks/next |
Plays that use it
| Play | What it does | Use it when |
|---|---|---|
| Review requests | Asks 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 references | Answers 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 referrals | Asks 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 follow | When 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.dev | Id |
|---|---|
| Customer | team: |
Example
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.