Answer keywords like MENU and HOURS automatically
Reply from your own number as soon as somebody texts a keyword, with keyword rules in the dashboard or with a webhook handler that looks the answer up in your own data.
Rosa runs a taco truck. Her sign says “Text MENU, HOURS or WHERE to 555-555-0100”, and while she’s cooking she can’t answer. She wants each of those words answered within seconds from her own number, and WHERE answered with wherever the truck is parked today, which changes every morning.
What you’ll have
- An answer from your number within seconds to anybody who texts one of your keywords.
- Everything else left for you to answer yourself.
- Stop words handled the usual way: STOP, UNSUBSCRIBE and the rest are never treated as keywords.
What you need
- A BlueBot account with your Mac connected, and a Mac that stays awake: an answer can only go while it’s on.
- Your keywords and their answers.
Let BlueBot see texts from new people
Keywords are mostly texted by people you’ve never texted. By default the Mac copies to BlueBot only the chats with people BlueBot has texted, so a stranger’s MENU would stay in Messages and never reach a rule. Open Settings in the dashboard and set Which chats to copy to Everything, or make this request:
/v1/settingscurl -X PATCH "https://usebluebot.com/api/v1/settings" \
-H "Authorization: Bearer $BLUEBOT_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"inbound_mode": "everything"
}'That copies every chat on your Mac, personal ones included, into BlueBot. If your business number is also your personal number, think about that before you turn it on.
There are two ways to build the replies. Keyword rules in the dashboard need no code and suit answers that stay the same. The API way is a webhook handler, for answers that come from your own data, such as today’s location.
With keyword rules
Keyword rules are set up in the dashboard; they have no API yet.
Make a rule for each keyword
In the dashboard, open Automations, then Keyword replies, and press New rule. Set it to match when the whole message is MENU. Capital letters don’t matter, so menu and Menu match too.
Choose Reply with a template and write the answer, or pick a saved template:
Hi from Rosa’s Tacos. Tacos $3.50 (carnitas, al pastor, pollo, hongos), burrito $11, quesadilla $9, horchata $4. Text WHERE to find the truck today.Add a rule for HOURS the same way:
We’re open Tuesday to Saturday, 11 AM to 2 PM and 5 PM to 9 PM. Text WHERE to find the truck today.Add what each rule does besides replying
A rule can also tag the person (menu-request) or enroll them in a . Tag MENU requests, so you know who asked when you post a special later.
Put the rules in order
The first rule that matches wins. Put whole-message rules above any contains or pattern rule, so a text like “menu please” isn’t caught by a broader rule first. Then save.
A text that matches no rule is left for you to answer from the inbox or your phone.
Keep WHERE out of the rules if it changes every day: a rule’s answer is fixed until you edit it. The API way below answers it from wherever you record the day’s spot.
With the API
The handler gets every incoming text through a webhook, reads it the way BlueBot reads stop words (trimmed, in lower case, without a full stop or an exclamation mark at the end), and answers the keywords it knows. The answer’s Idempotency-Key is the incoming text’s id, so a webhook delivered twice is answered once.
Make a webhook for incoming texts
In the dashboard, open Developers, then Webhooks, and add your handler’s address with the message.received event. Copy the signing secret it shows once.
/v1/webhookscurl -X POST "https://usebluebot.com/api/v1/webhooks" \
-H "Authorization: Bearer $BLUEBOT_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://rosastacos.example/hooks/bluebot",
"events": [
"message.received"
],
"description": "Keyword replies"
}'Write the handler
// app/hooks/bluebot/route.ts (Next.js)
import BlueBot, { WebhookVerificationError } from '@usebluebot/sdk';
import { todaysSpot } from '@/lib/schedule'; // your own data: where the truck is today
const client = new BlueBot({ apiKey: process.env.BLUEBOT_API_KEY });
const ANSWERS: Record<string, () => Promise<string> | string> = {
menu: () => 'Hi from Rosa’s Tacos. Tacos $3.50 (carnitas, al pastor, pollo, hongos), burrito $11, quesadilla $9, horchata $4. Text WHERE to find the truck today.',
hours: () => 'We’re open Tuesday to Saturday, 11 AM to 2 PM and 5 PM to 9 PM. Text WHERE to find the truck today.',
where: async () => {
const spot = await todaysSpot();
return spot ? `Today we’re at ${spot.place} until ${spot.until}. See you there.` : 'We’re closed today. Text HOURS for when we’re open.';
},
};
// The way BlueBot reads a stop word: trimmed, lower case, no full stop or exclamation mark at the end.
const keywordOf = (text: string | null) => (text ?? '').trim().toLowerCase().replace(/[.!]+$/, '');
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;
}
if (event.type !== 'message.received') return new Response(null, { status: 204 });
const message = event.data;
const answer = ANSWERS[keywordOf(message.text)];
if (!answer || message.chatId || !message.from) return new Response(null, { status: 204 }); // not a keyword, or a group chat
await client.messages.send(
{ to: message.from, text: await answer(), metadata: { keyword: keywordOf(message.text), in_reply_to: message.id } },
{ idempotencyKey: `keyword-${message.id}` },
);
return new Response(null, { status: 204 });
}The answer to a WHERE, 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: keyword-msg_2Bf8QzD4kJmA6nXwEuTr1c" \
-d '{
"to": "+15555550123",
"text": "Today we’re at Pier 7, by the ferry, until 2 PM. See you there.",
"metadata": {
"keyword": "where",
"in_reply_to": "msg_2Bf8QzD4kJmA6nXwEuTr1c"
}
}'Leave stop words alone
The handler needs no code for STOP: BlueBot has already put the person on your stop list by the time your webhook hears about it, and it cancels any text to them, so a keyword in your map that happened to be a stop word would never be answered anyway. Keep stop, start, yes and the other stop and start words out of your map so nobody is surprised.
Answer texts as fast as you can, but answer the webhook first if your lookup is slow: BlueBot waits 10 seconds for a reply before it counts the delivery as failed and tries again. The Idempotency-Key makes the retry safe either way.
Test it
Text MENU to your BlueBot number from your own phone, then HOURS, then a text that isn’t a keyword.
| You got | It means | Do next |
|---|---|---|
| The menu on your phone within seconds | It works. | Try Menu. and menu! too. |
| Nothing, and your MENU text isn’t in BlueBot’s inbox | The Mac didn’t copy it: Which chats to copy is on People BlueBot has texted, and BlueBot has never texted your phone. | Set it to Everything, as above. |
| Your text is in the inbox, but no answer | No rule matched, or your handler didn’t answer. | Check the rule matches the whole message. For the API way, the webhook’s deliveries in Developers, then Webhooks show what your handler answered. |
The answer waits with hold.reason: "quiet_hours" | It’s quiet hours, and answers wait like any text. | Shorten or turn off quiet hours if people text late, in Settings, then Sending rules. |
| Two answers to one MENU | Two rules matched, or a rule and your handler both answered. | Use one way per keyword. |
When it goes wrong
- The Mac is asleep. A text that arrives while the Mac sleeps is read when it wakes, and the answer goes then. Keep a Mac that answers keywords awake: in System Settings, Energy, stop it sleeping when the display is off.
- A keyword in a sentence. “What’s on the menu?” doesn’t match a whole-message rule. Add a contains rule for
menubelow the whole-message ones if you want it answered, and expect it to catch “I loved the menu” too. - Somebody texts MENU every five minutes. Each one is answered. A handler can remember when it last answered a person and skip a repeat within an hour.
- Quiet hours. A keyword answer is a text like any other and waits through quiet hours. For a business that’s open late, set quiet hours to match.