Follow up three times and stop when they reply
Send three texts over a week from your own number and stop as soon as the person answers, with a Sequence in the dashboard or with scheduled texts that your webhook cancels on a reply.
Tanya is a real estate agent. Everyone who signs in at her open houses gets a text that evening, and most never answer it. The ones who buy a house later say they meant to. She wants a short follow-up two days later and a last one a week later, and she wants them to stop as soon as somebody writes back, because “Still looking?” after “Yes, can we see the one on Elm?” makes her look like a robot.
What you’ll have
- Three texts from your number: one now, one two days later and one a week later.
- Nothing more sent to anybody who replies, at whatever step they reply.
- Nothing more sent to anybody who texts STOP, which BlueBot does for you.
What you need
- A BlueBot account with your Mac connected.
- People who asked to hear from you, such as visitors who signed in and gave their number for follow-up.
There are two ways. A Sequence in the dashboard does the whole thing with no code, and it’s the one to use unless you need the texts to come from your own system. The API way schedules all three texts at once and cancels the rest from your webhook when a reply comes in.
With a Sequence
A Sequence is a list of texts with waits between them, stopped for each person who replies or texts STOP. Sequences are built and run in the dashboard, and people are enrolled from the dashboard, a keyword rule, a Text Hook or Zapier’s Enroll in Sequence action. They have no API yet.
Make the sequence
In the dashboard, open Sequences and press New sequence. Name it “Open house follow-up”.
Add the three texts
Add a step for each text, with the wait before it:
| Step | Wait before it | Text |
|---|---|---|
| 1 | None | Hi {{first_name}}, it’s Tanya Brooks. Thanks for stopping by 412 Oak St today. Anything you’d like to know about it? |
| 2 | 2 days | Hi {{first_name}}, Tanya again. If Oak St wasn’t quite right, I have two similar homes coming up this month. Want the details? |
| 3 | 5 days | Last one from me, {{first_name}}. If you’re still looking, reply here and I’ll send you new listings as they come up. |
{{first_name}} comes from the contact. A contact without a first name gets the text without it, so read each text with the name left out and make sure it still reads well.
Keep it to sensible hours
Turn on the send window and choose weekdays and Saturday, 9 AM to 6 PM. A step that comes due at night waits for the window. Your quiet hours apply on top of it.
Check how it stops
Stop when they reply is on by default; leave it on. A reply at any step ends the sequence for that person, and a text that was waiting for them is canceled with cancel_reason: "sequence_stopped". A STOP ends it too, and puts them on your stop list.
Enroll people
Enroll the people from today’s open house: in Contacts, tick them and choose Enroll in sequence, or upload the sign-in sheet as a CSV with the tag open-house-oak and enroll everybody with that tag. To enroll each visitor as they sign in on a tablet form, use a with Enroll in a sequence turned on, or Zapier’s Enroll in Sequence action after your form’s trigger.
When a person finishes the sequence, or it stops early for them, BlueBot sends the sequence.completed event with the reason and the person’s contact (their number as handle, and their name, tags and fields under contact), which you can use to move them along in your CRM without looking them up. To see whether somebody is in a sequence now, list its people with GET /v1/sequences/{id}/enrollments?contact_id=ct_….
With the API
Here your own system sends the texts: it schedules all three when a person signs in, and a webhook handler cancels whatever is still scheduled when that person replies. Each text carries metadata.drip with the drip’s name, so the handler cancels only the drip’s texts and leaves any appointment reminder alone.
Schedule the three texts
Send each with its own send_at. The first goes now. Every text gets an Idempotency-Key built from the visitor and the step, so a retry never sends a step twice.
// drip.ts
import BlueBot from '@usebluebot/sdk';
const client = new BlueBot({ apiKey: process.env.BLUEBOT_API_KEY });
const DAY = 24 * 60 * 60 * 1000;
const STEPS = [
{ after: 0, text: (name: string) => `Hi ${name}, it’s Tanya Brooks. Thanks for stopping by 412 Oak St today. Anything you’d like to know about it?` },
{ after: 2 * DAY, text: (name: string) => `Hi ${name}, Tanya again. If Oak St wasn’t quite right, I have two similar homes coming up this month. Want the details?` },
{ after: 7 * DAY, text: (name: string) => `Last one from me, ${name}. If you’re still looking, reply here and I’ll send you new listings as they come up.` },
];
export async function startDrip(visitor: { id: string; firstName: string; mobile: string }) {
const start = Date.now();
for (const [index, step] of STEPS.entries()) {
await client.messages.send(
{
to: visitor.mobile,
text: step.text(visitor.firstName),
// The first goes now; the others are scheduled.
...(step.after ? { sendAt: new Date(start + step.after).toISOString() } : {}),
metadata: { drip: 'open-house-oak', visitor_id: visitor.id, step: String(index + 1) },
},
{ idempotencyKey: `drip-open-house-oak-${visitor.id}-${index + 1}` },
);
}
}The second step, as a request:
/v1/messagescurl -X POST "https://usebluebot.com/api/v1/messages" \
-H "Authorization: Bearer $BLUEBOT_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: drip-open-house-oak-v_2291-2" \
-d '{
"to": "+15555550123",
"text": "Hi Maria, Tanya again. If Oak St wasn’t quite right, I have two similar homes coming up this month. Want the details?",
"send_at": "2026-11-15T23:00:00.000Z",
"metadata": {
"drip": "open-house-oak",
"visitor_id": "v_2291",
"step": "2"
}
}'Make a webhook for replies
In the dashboard, open Developers, then Webhooks, and add your handler’s address with the message.received and reaction.added events. Copy the signing secret it shows once. A tapback such as a heart on your text arrives as reaction.added, not as a text; this handler counts it as a reply, since a person who hearted your text has seen it.
/v1/webhookscurl -X POST "https://usebluebot.com/api/v1/webhooks" \
-H "Authorization: Bearer $BLUEBOT_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://tanyabrookshomes.example/hooks/bluebot",
"events": [
"message.received",
"reaction.added"
],
"description": "Stop drips on reply"
}'Cancel the rest on a reply
When a text or a tapback comes in from somebody, the handler lists the texts still scheduled for them and cancels the ones that belong to a drip. Texts in a group chat are left alone, since a reply there isn’t an answer to your one-to-one follow-up.
// app/hooks/bluebot/route.ts (Next.js)
import BlueBot, { WebhookVerificationError } from '@usebluebot/sdk';
const client = new BlueBot({ apiKey: process.env.BLUEBOT_API_KEY });
async function stopDrips(person: string) {
const page = await client.messages.list({ person, state: 'scheduled', limit: 100 });
for (const message of page.data) {
if (!message.metadata.drip) continue; // not part of a drip: leave it
try {
await client.messages.cancel(message.id);
} catch (error) {
if ((error as { code?: string }).code !== 'conflict') throw error; // it became due and went a moment ago
}
}
}
export async function POST(request: Request) {
const payload = await request.text();
let event;
try {
event = client.webhooks.verify(payload, request.headers, process.env.BLUEBOT_WEBHOOK_SECRET);
} catch (error) {
if (error instanceof WebhookVerificationError) return new Response('Bad signature', { status: 400 });
throw error;
}
const fromThem = event.type === 'message.received' || (event.type === 'reaction.added' && event.data.direction === 'in');
if (fromThem && !event.data.chatId && event.data.from) await stopDrips(event.data.from);
return new Response(null, { status: 204 });
}The handler’s two requests, for a reply from +15555550123:
/v1/messagescurl "https://usebluebot.com/api/v1/messages?person=%2B15555550123&state=scheduled&limit=100" \
-H "Authorization: Bearer $BLUEBOT_API_KEY"/v1/messages/msg_1Hq2LxW9cVbN3mKpRtYs0a/cancelcurl -X POST "https://usebluebot.com/api/v1/messages/msg_1Hq2LxW9cVbN3mKpRtYs0a/cancel" \
-H "Authorization: Bearer $BLUEBOT_API_KEY"BlueBot may deliver the same event twice, for instance when your handler is slow to answer. Canceling twice is harmless: the second cancel of a canceled text answers 409 conflict, which the handler ignores.
Test it
Enroll yourself, or call startDrip with your own number and the waits shortened to two minutes and four minutes. When the first text arrives, wait for the second, then reply to it.
| You got | It means | Do next |
|---|---|---|
| The first text now and the second two minutes later | The schedule works. | Reply to the second text. |
After your reply, the third text in Messages as canceled | Stopping works. With a Sequence, its cancel_reason is sequence_stopped; with your handler, requested. | Put the waits back to two days and five days. |
| The third text arrived anyway | Your reply didn’t stop it. | For a Sequence, check Stop when they reply is on. For the API way, look at the webhook’s deliveries in Developers, then Webhooks: a failed delivery or a 400 from your handler means it never canceled. |
| Your reply isn’t in BlueBot at all | Your Mac didn’t copy it. | Check your Mac is online. Replies to texts BlueBot sent are always copied. |
| All three texts went at once | Each text needs its own send_at. | Check the second and third carry one. |
When it goes wrong
- Somebody replies “STOP”. BlueBot puts them on your stop list and cancels every text still waiting for them, drip or not. You don’t need a rule for it.
- Somebody replies “Who is this?”. That’s a reply, so the drip stops, which is right: answer them yourself.
- The visitor gave a landline. The first text fails with a
failure.reason, and the next two would fail the same way. Cancel them, or cancel all scheduled texts for any person whose drip text fails, from amessage.failedhandler. - Fifty people sign in at one open house. Every first text goes in one evening, 20 seconds apart, and new people past your daily limit wait until tomorrow. For a big event, start the drip the next morning instead.