# Change, cancel or settle a text

> Change a scheduled text, cancel one that hasn’t gone, and settle an unconfirmed text the Mac couldn’t confirm, with which states allow which.

A text can be changed while it’s scheduled, canceled while it’s waiting, and settled by you when the Mac couldn’t tell whether it went. After it goes, it’s in the person’s Messages and nobody can take it back.

## What each state allows

| State                                            | Change it (`PATCH`) | Cancel it                | Settle it (`resolve`) |
| ------------------------------------------------ | ------------------- | ------------------------ | --------------------- |
| `scheduled`                                      | Yes                 | Yes                      | No                    |
| `queued`                                         | No                  | Yes                      | No                    |
| `sending`                                        | No                  | No: the Mac has it       | No                    |
| `unconfirmed`                                    | No                  | No                       | Yes                   |
| `canceled`                                       | No                  | Yes, and nothing changes | No                    |
| `sent`, `delivered`, `read`, `failed`, `expired` | No                  | No                       | No                    |

Asking for something a state doesn’t allow answers `409 conflict`, and `error.message` names the state the text is in. Texts that came in (`received`) can’t be changed, canceled or settled. [Message states](https://usebluebot.com/docs/concepts/message-states)

## Change a scheduled text

`PATCH /v1/messages/{id}` changes the words, the time, the expiry or the metadata of a text that is still `scheduled`. Send only what changes:

```bash
curl -X PATCH "https://usebluebot.com/api/v1/messages/msg_1Hq2LxW9cVbN3mKpRtYs0a" \
  -H "Authorization: Bearer $BLUEBOT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "text": "Hi Maria, your cleaning with Dr. Patel moved to Friday at 2 PM. Reply C to confirm.",
    "send_at": "2026-11-20T17:00:00-05:00"
  }'
```

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

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

const message = await client.messages.update('msg_1Hq2LxW9cVbN3mKpRtYs0a', {
  text: 'Hi Maria, your cleaning with Dr. Patel moved to Friday at 2 PM. Reply C to confirm.',
  sendAt: '2026-11-20T17:00:00-05:00',
});
console.log(message);
```

```python
import os
from bluebot import BlueBot

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

message = client.messages.update(
    "msg_1Hq2LxW9cVbN3mKpRtYs0a",
    text="Hi Maria, your cleaning with Dr. Patel moved to Friday at 2 PM. Reply C to confirm.",
    send_at="2026-11-20T17:00:00-05:00",
)
print(message)
```

| Field        | What it does                                                                                                                                                       |
| ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `text`       | New words, up to 2,000 characters. `null` removes them, which only works on a text with files.                                                                     |
| `send_at`    | A new time. Now or in the past sends the text at once: it becomes `queued` and `message.queued` fires.                                                             |
| `expires_at` | A new expiry, after the text is due. `null` puts back the default from your settings. A new `send_at` with no `expires_at` moves the default expiry along with it. |
| `metadata`   | Replaces the whole metadata object, so send every key you want to keep.                                                                                            |

You can’t change who a text goes to, its service or its files. Cancel it and send a new one instead.

The answer is the changed message. A scheduled text that becomes due fires `message.queued`; otherwise a change fires no event.

| You got                           | It means                                                                         | Do next                                                           |
| --------------------------------- | -------------------------------------------------------------------------------- | ----------------------------------------------------------------- |
| `200` with `"state": "scheduled"` | Changed. It goes at the new `send_at`.                                           | Nothing.                                                          |
| `200` with `"state": "queued"`    | The new `send_at` was now or past, so it’s going.                                | Nothing, unless you meant a later time.                           |
| `409 conflict`                    | It isn’t scheduled any more.                                                     | If it’s `queued`, cancel it and send the new words as a new text. |
| `400 invalid_request`             | An empty body, words over 2,000 characters, or an expiry before the text is due. | Fix the field `error.details` names.                              |
| `404 not_found`                   | No text with that id on your account, or a text that came in.                    | Check the `msg_` id.                                              |

## Cancel a text

`POST /v1/messages/{id}/cancel` stops a text that is `scheduled` or `queued`:

```bash
curl -X POST "https://usebluebot.com/api/v1/messages/msg_1Hq2LxW9cVbN3mKpRtYs0a/cancel" \
  -H "Authorization: Bearer $BLUEBOT_API_KEY" \
  -H "Idempotency-Key: cancel-msg_1Hq2LxW9cVbN3mKpRtYs0a"
```

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

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

const message = await client.messages.cancel('msg_1Hq2LxW9cVbN3mKpRtYs0a', { idempotencyKey: 'cancel-msg_1Hq2LxW9cVbN3mKpRtYs0a' });
console.log(message);
```

```python
import os
from bluebot import BlueBot

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

message = client.messages.cancel(
    "msg_1Hq2LxW9cVbN3mKpRtYs0a",
    idempotency_key="cancel-msg_1Hq2LxW9cVbN3mKpRtYs0a",
)
print(message)
```

The answer, `200`:

```json
{
  "id": "msg_1Hq2LxW9cVbN3mKpRtYs0a",
  "object": "message",
  "state": "canceled",
  "cancel_reason": "requested",
  "canceled_at": "2026-11-13T19:42:00.000Z",
  "hold": null
}
```

The text never goes, and `message.canceled` fires. Canceling a text that is already canceled answers it again with nothing changed, so it’s safe to retry. A text in `sending` can’t be canceled: the Mac took it, usually within the last few seconds, and it is already on its way to Messages.

`cancel_reason` says who canceled a text:

| `cancel_reason`      | Who canceled it                                                                              |
| -------------------- | -------------------------------------------------------------------------------------------- |
| `requested`          | You, through the API, the dashboard or an integration.                                       |
| `opted_out`          | BlueBot, because the person is on your stop list.                                            |
| `sequence_stopped`   | BlueBot, because the sequence the text belonged to stopped, such as when the person replied. |
| `broadcast_canceled` | You, by canceling the broadcast it belonged to.                                              |
| `staff`              | Someone at BlueBot, such as support acting on your request.                                  |

To stop everything for a while rather than cancel texts one by one, pause sending instead. Paused texts wait with the `paused` hold and go when you resume, unless they expire first. [Holds](https://usebluebot.com/docs/concepts/holds)

## Settle an unconfirmed text

When the Mac takes a text, it has 3 minutes to report what happened. Usually it reports `sent` within seconds. If those 3 minutes pass with no report, the text becomes `unconfirmed` and `message.unconfirmed` fires.

That happens when the Mac lost its connection, went to sleep or restarted partway, or when Messages still hadn’t shown the text as sent 30 seconds after the Mac handed it over. A regular text going through an iPhone that’s out of reach is the usual case. The text may have gone, or it may not have.

BlueBot never sends an unconfirmed text again on its own, because sending a text twice is worse than asking you. You settle it with `POST /v1/messages/{id}/resolve`:

1. Look at the conversation in Messages on your Mac or iPhone.
2. If the text is there and shows as sent, mark it sent.
3. If it isn’t, or it shows “Not Delivered”, send it again.

```bash
curl -X POST "https://usebluebot.com/api/v1/messages/msg_1Hq2LxW9cVbN3mKpRtYs0a/resolve" \
  -H "Authorization: Bearer $BLUEBOT_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: settle-msg_1Hq2LxW9cVbN3mKpRtYs0a" \
  -d '{
    "action": "mark_sent"
  }'
```

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

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

const message = await client.messages.resolve('msg_1Hq2LxW9cVbN3mKpRtYs0a', {
  action: 'mark_sent',
}, { idempotencyKey: 'settle-msg_1Hq2LxW9cVbN3mKpRtYs0a' });
console.log(message);
```

```python
import os
from bluebot import BlueBot

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

message = client.messages.resolve(
    "msg_1Hq2LxW9cVbN3mKpRtYs0a",
    action="mark_sent",
    idempotency_key="settle-msg_1Hq2LxW9cVbN3mKpRtYs0a",
)
print(message)
```

| `action`     | What happens                                                                                                                                                                                                                                |
| ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `mark_sent`  | The text becomes `sent` with `"resolution": "marked_sent"`, and `message.sent` fires.                                                                                                                                                       |
| `send_again` | BlueBot queues a new text with the same words, files, service and metadata. The original stays `unconfirmed` with `"resolution": "resent"`, and `resent_message_id` names the new text, which goes like any other and fires its own events. |

```bash
curl -X POST "https://usebluebot.com/api/v1/messages/msg_1Hq2LxW9cVbN3mKpRtYs0a/resolve" \
  -H "Authorization: Bearer $BLUEBOT_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: settle-msg_1Hq2LxW9cVbN3mKpRtYs0a-again" \
  -d '{
    "action": "send_again"
  }'
```

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

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

const message = await client.messages.resolve('msg_1Hq2LxW9cVbN3mKpRtYs0a', {
  action: 'send_again',
}, { idempotencyKey: 'settle-msg_1Hq2LxW9cVbN3mKpRtYs0a-again' });
console.log(message);
```

```python
import os
from bluebot import BlueBot

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

message = client.messages.resolve(
    "msg_1Hq2LxW9cVbN3mKpRtYs0a",
    action="send_again",
    idempotency_key="settle-msg_1Hq2LxW9cVbN3mKpRtYs0a-again",
)
print(message)
```

The answer, `200`:

```json
{
  "id": "msg_1Hq2LxW9cVbN3mKpRtYs0a",
  "object": "message",
  "state": "unconfirmed",
  "resolution": "resent",
  "resolved_at": "2026-11-13T19:50:00.000Z",
  "resent_message_id": "msg_2Bf8QzD4kJmA6nXwEuTr1c"
}
```

Settle a text once. Settling it again, or settling a text that isn’t `unconfirmed`, answers `409 conflict`. Send an `Idempotency-Key` with `send_again` so a retry can’t queue two new texts.

If a late report comes in from the Mac saying the text went after all, BlueBot records that it was sent, so a text you haven’t settled yet may settle itself.

### Find texts that need you

`GET /v1/device` counts them in `unconfirmed_count`, and BlueBot’s window on the Mac lists them under **Needs you**. To read them:

```bash
curl "https://usebluebot.com/api/v1/messages?state=unconfirmed" \
  -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({
  state: 'unconfirmed',
});
console.log(page.data);
```

```python
import os
from bluebot import BlueBot

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

page = client.messages.list(state="unconfirmed")
print(page.data)
```

A program that sends many texts should listen for `message.unconfirmed` and either ask a person or follow a rule you choose. A reminder that has a later reminder coming can usually be marked sent. A one-time code or a payment link should be checked by a person.

| You got                                                        | It means                                           | Do next                                   |
| -------------------------------------------------------------- | -------------------------------------------------- | ----------------------------------------- |
| `200` with `"state": "sent"` and `"resolution": "marked_sent"` | Recorded as sent.                                  | Nothing.                                  |
| `200` with `"resolution": "resent"`                            | A new text is queued.                              | Follow `resent_message_id` for its state. |
| `409 conflict`                                                 | It was already settled, or it isn’t `unconfirmed`. | Read the text to see its state.           |

## What you can’t do

BlueBot can’t edit a text after it’s sent, unsend it, or send a tapback. Messages allows those only through private frameworks in macOS that need System Integrity Protection turned off, and BlueBot won’t ask that of anyone. You can still edit or unsend a text by hand in Messages on your Mac or iPhone, and BlueBot records the change with `message.edited` or `message.unsent`. [Capabilities](https://usebluebot.com/docs/concepts/capabilities)
