# 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:

```bash
curl -X PATCH "https://usebluebot.com/api/v1/settings" \
  -H "Authorization: Bearer $BLUEBOT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "inbound_mode": "everything"
  }'
```

```ts
import BlueBot from '@usebluebot/sdk';

const client = new BlueBot({ apiKey: process.env.BLUEBOT_API_KEY });

const settings = await client.settings.update({
  inboundMode: 'everything',
});
console.log(settings);
```

```python
import os
from bluebot import BlueBot

client = BlueBot(api_key=os.environ["BLUEBOT_API_KEY"])

settings = client.settings.update(inbound_mode="everything")
print(settings)
```

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

The answer, `200`:

```json
{
  "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. [Files and voice memos](https://usebluebot.com/docs/sending/files)              |
| `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. [Group chats](https://usebluebot.com/docs/sending/group-chats)         |
| `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:

```bash
curl -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"
  }'
```

```ts
import BlueBot from '@usebluebot/sdk';

const client = new BlueBot({ apiKey: process.env.BLUEBOT_API_KEY });

const webhook = await client.webhooks.create({
  url: 'https://example.com/webhooks/bluebot',
  events: ['message.received'],
  description: 'Replies into the CRM',
}, { idempotencyKey: 'crm-replies-webhook' });
console.log(webhook);
```

```python
import os
from bluebot import BlueBot

client = BlueBot(api_key=os.environ["BLUEBOT_API_KEY"])

webhook = client.webhooks.create(
    url="https://example.com/webhooks/bluebot",
    events=["message.received"],
    description="Replies into the CRM",
    idempotency_key="crm-replies-webhook",
)
print(webhook)
```

Each delivery is the event, with the message in `data`:

```json title="The body BlueBot posts"
{
  "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:

```ts title="app/webhooks/bluebot/route.ts"
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 });
}
```

```python title="webhooks.py"
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
```

[Webhooks](https://usebluebot.com/docs/webhooks) covers setting one up, [Verify signatures](https://usebluebot.com/docs/webhooks/verify) the check in every language, and [Test webhooks locally](https://usebluebot.com/docs/webhooks/testing) 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:

```bash
curl "https://usebluebot.com/api/v1/messages?direction=in&since=2026-11-13T00%3A00%3A00Z" \
  -H "Authorization: Bearer $BLUEBOT_API_KEY"
```

```ts
import BlueBot from '@usebluebot/sdk';

const client = new BlueBot({ apiKey: process.env.BLUEBOT_API_KEY });

const page = await client.messages.list({
  direction: 'in',
  since: '2026-11-13T00:00:00Z',
});
console.log(page.data);
```

```python
import os
from bluebot import BlueBot

client = BlueBot(api_key=os.environ["BLUEBOT_API_KEY"])

page = client.messages.list(direction="in", since="2026-11-13T00:00:00Z")
print(page.data)
```

Narrow it with `person` (a number or an address), `contact_id`, `chat_id` or `until`, and page through with `cursor`. [Pagination](https://usebluebot.com/docs/concepts/pagination)

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:

```bash
curl "https://usebluebot.com/api/v1/events?type=message.received&since=2026-11-13T00%3A00%3A00Z" \
  -H "Authorization: Bearer $BLUEBOT_API_KEY"
```

```ts
import BlueBot from '@usebluebot/sdk';

const client = new BlueBot({ apiKey: process.env.BLUEBOT_API_KEY });

const page = await client.events.list({
  type: 'message.received',
  since: '2026-11-13T00:00:00Z',
});
console.log(page.data);
```

```python
import os
from bluebot import BlueBot

client = BlueBot(api_key=os.environ["BLUEBOT_API_KEY"])

page = client.events.list(type="message.received", since="2026-11-13T00:00:00Z")
print(page.data)
```

## 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`.

```json title="data on reaction.added"
{
  "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. [Capabilities](https://usebluebot.com/docs/concepts/capabilities)

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. [Stop words](https://usebluebot.com/docs/concepts/stop-words)

## 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. [Data retention](https://usebluebot.com/docs/concepts/data-retention)
