Receive texts
How replies reach BlueBot, which chats your Mac copies, and how to hear about each text that comes in, with tapbacks, edits and unsends.
Your Mac also reads Messages and copies texts to BlueBot: the replies people send you, the texts you type by hand on your Mac or iPhone, tapbacks, edits and unsends. Each text that comes in becomes a message with "direction": "in" and "state": "received", and BlueBot tells your webhooks with message.received.
Nothing changes on your phone. Replies still arrive in Messages on your iPhone and Mac the way they always have, and you can answer them there. BlueBot sees your answer too.
Which chats are copied
Your Mac copies some chats and leaves the rest alone. Which ones is the Which chats to copy setting: People BlueBot has texted or Everything in Settings → Sending rules, the same choice in the Mac app’s own settings, and inbound_mode in the API:
inbound_mode | The Mac copies |
|---|---|
replies (the default) | One-to-one chats with people you’ve texted through BlueBot, and group chats in which you’ve texted everyone else through BlueBot. |
everything | Every chat on the Mac: one-to-one and group, whoever started it. |
“Texted through BlueBot” means any text BlueBot sent for you: from the API, the dashboard, Zapier, a sequence or anywhere else. A chat counts from the first text BlueBot sends into it, so the first reply to that text is copied.
Under replies, somebody who texts you first is not copied. If people find your number on a sign, an ad or your website and text you before you’ve ever texted them, set Which chats to copy to Everything, or BlueBot never hears from them. Under everything your personal chats are copied as well. BlueBot deletes the words of every text after 30 days, but think about whose chats are on that Mac before you choose it.
In either mode, the Mac never copies chats with short codes (the 5- and 6-digit numbers banks and airlines text from) or business chats, since those aren’t a phone number or an email address.
Change it under Settings → Sending rules in the dashboard, in BlueBot’s window on the Mac, or with the API:
/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"
}'The Mac picks up the change at its next check-in, within 10 seconds. When you first connect a Mac, it copies the chats it should from the 7 days before, so a fresh account starts with recent history.
What a text in looks like
{
"id": "msg_3Lp3VsC7hGqE5tZyKbWn2d",
"object": "message",
"direction": "in",
"state": "received",
"to": "+15555550100",
"from": "+15555550123",
"chat_id": null,
"contact_id": "ct_1Hq2LxW9cVbN3mKpRtYs0a",
"conversation_id": "conv_1Hq2LxW9cVbN3mKpRtYs0a",
"text": "Perfect, see you there at 3",
"files": [],
"service": "imessage",
"source": "mac",
"metadata": {},
"reply_to_id": null,
"reactions": [],
"edited_at": null,
"unsent_at": null,
"received_at": "2026-11-13T19:44:10.000Z"
}| Field | What it tells you |
|---|---|
from | Who sent it, in E.164 (+15555550123) or as an email address. Look the person up in your own system by this. |
to | Your own number, when the Mac can tell it. |
contact_id, conversation_id | The same ids the texts you sent this person carry, so a reply joins its conversation without any matching on your side. |
text | The words. null for a text of only pictures or files, for one that was unsent, and after 30 days. |
files | Pictures, voice memos and other files, each with a link that works for an hour. |
service | imessage or sms: what the person texted you with. |
reply_to_id | The text this one answers, when the person used Messages’ inline reply. |
chat_id | Set for a text in a group chat. contact_id and from are then the member who sent it. |
metadata | Always {} on a text that came in. Your metadata rides on the texts you send, so match a reply to your record by contact_id or from. |
Hear about each one
Point a webhook at your server for message.received, and BlueBot posts each text that comes in, signed so you can check it came from BlueBot:
/v1/webhookscurl -X POST "https://usebluebot.com/api/v1/webhooks" \
-H "Authorization: Bearer $BLUEBOT_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: crm-replies-webhook" \
-d '{
"url": "https://example.com/webhooks/bluebot",
"events": [
"message.received"
],
"description": "Replies into the CRM"
}'Each delivery is the event, with the message in data:
{
"id": "evt_1Hq2LxW9cVbN3mKpRtYs0a",
"object": "event",
"type": "message.received",
"api_version": "2026-11-13",
"timestamp": "2026-11-13T19:44:12.000Z",
"data": {
"id": "msg_3Lp3VsC7hGqE5tZyKbWn2d",
"object": "message",
"direction": "in",
"state": "received",
"from": "+15555550123",
"text": "Perfect, see you there at 3"
}
}A handler only has to check the signature, look at type, and answer 200 quickly:
import BlueBot from '@usebluebot/sdk';
const client = new BlueBot({ apiKey: process.env.BLUEBOT_API_KEY });
export async function POST(request: Request) {
const payload = await request.text();
// Throws WebhookVerificationError when the signature doesn't match.
const event = client.webhooks.verify(payload, request.headers, process.env.BLUEBOT_WEBHOOK_SECRET!);
if (event.type === 'message.received') {
const message = event.data;
await saveReply({ from: message.from, text: message.text, contactId: message.contactId });
}
return new Response(null, { status: 200 });
}import os
from bluebot import BlueBot
client = BlueBot(api_key=os.environ["BLUEBOT_API_KEY"])
def handle(payload: bytes, headers: dict) -> int:
# Raises bluebot.WebhookVerificationError when the signature doesn't match.
event = client.webhooks.verify(payload, headers, os.environ["BLUEBOT_WEBHOOK_SECRET"])
if event.type == "message.received":
message = event.data
save_reply(from_=message.from_, text=message.text, contact_id=message.contact_id)
return 200 covers setting one up, the check in every language, and how to run a handler on your laptop with bluebot listen.
When message.received fires
BlueBot waits until the Mac has uploaded the text’s pictures and files, which usually takes seconds, so the event carries files you can download. If they haven’t come after 2 minutes, the event goes anyway with those files’ status set to waiting; read the message again later for them.
A text arrives on your Mac when Messages gets it, and the Mac copies new texts at each check-in, every 10 seconds. A reply usually reaches your webhook within 10 to 20 seconds of reaching your phone. While the Mac is asleep or off, nothing is copied. When it comes back, it copies everything it missed, in order, and each text fires its event then. received_at is when the text really arrived, so use that rather than the time your webhook was called.
List incoming texts
To read texts in rather than be told, list them. Lists are newest first:
/v1/messagescurl "https://usebluebot.com/api/v1/messages?direction=in&since=2026-11-13T00%3A00%3A00Z" \
-H "Authorization: Bearer $BLUEBOT_API_KEY"Narrow it with person (a number or an address), contact_id, chat_id or until, and page through with cursor.
To catch up on what a webhook missed, read the event feed, which keeps 30 days of events in the shape a webhook gets them:
/v1/eventscurl "https://usebluebot.com/api/v1/events?type=message.received&since=2026-11-13T00%3A00%3A00Z" \
-H "Authorization: Bearer $BLUEBOT_API_KEY"Texts you type by hand
The Mac copies both directions of every chat it copies, so a text you type on your Mac or iPhone shows up too, as "direction": "out" with "source": "mac". It fires message.sent like a text BlueBot sent. Your code can tell the two apart by source, and the dashboard shows the whole conversation the way your phone does.
Texts you type by hand don’t count against your daily limits and aren’t paced: BlueBot only records them.
Tapbacks
When somebody loves, likes or laughs at a text (a tapback), BlueBot records it on that text’s reactions and fires reaction.added. Taking one back fires reaction.removed.
{
"id": "rxn_1Hq2LxW9cVbN3mKpRtYs0a",
"object": "reaction",
"kind": "love",
"emoji": null,
"from": "+15555550123",
"direction": "in",
"added_at": "2026-11-13T19:45:00.000Z",
"message_id": "msg_1Hq2LxW9cVbN3mKpRtYs0a",
"conversation_id": "conv_1Hq2LxW9cVbN3mKpRtYs0a",
"chat_id": null,
"contact_id": "ct_1Hq2LxW9cVbN3mKpRtYs0a",
"removed_at": null
}kind is love, like, dislike, laugh, emphasize, question, or emoji with the emoji in emoji. direction is in for the other person’s and out for one you added on your own phone. BlueBot can read tapbacks but can’t send them.
A tapback is not a text, so it doesn’t fire message.received and isn’t in GET /v1/messages.
Edits and unsends
On macOS 13 or later, when somebody edits a text, BlueBot updates its text, sets edited_at and fires message.edited. When somebody unsends one, BlueBot clears its words, sets unsent_at and fires message.unsent. Both work for your own texts too. If your app showed the old words somewhere, update or remove them when these events come.
Stop words
When somebody texts you one-to-one with only a stop word, such as STOP or Unsubscribe, BlueBot still copies the text and fires message.received. While stop words are on (the default), it also puts them on your stop list, cancels any texts still waiting for them and fires contact.stopped. A reply of START, UNSTOP or YES takes them off again. Stop words in a group chat do nothing, since BlueBot can’t leave one person out of a group.
What BlueBot keeps
BlueBot keeps the words of a text, and the files on it, for 30 days. After that the message stays with its facts (who, when, which service, the tapbacks) and "text_deleted": true. If you need the words for longer, copy them into your own system when message.received comes.