# Message states

> Every state a text can be in, every move between them and the webhook each move fires, why a text failed or was canceled, and how to settle one BlueBot couldn't confirm.

Every message has a `state`. A text you send moves forward through a few of them and stops at one that's settled; a text that comes in is `received` and stays there. Each move fires a webhook, so you never need to poll to find out what happened.

```mermaid
stateDiagram-v2
  [*] --> scheduled: send_at in the future
  [*] --> queued
  scheduled --> queued: send_at reached
  scheduled --> canceled
  queued --> sending: the Mac claims it
  queued --> canceled
  queued --> expired: expires_at passed
  sending --> sent
  sending --> failed
  sending --> unconfirmed: lease ran out
  unconfirmed --> sent: mark_sent
  sent --> delivered: iMessage only
  delivered --> read: read receipts on
  [*] --> received: a text in
```

## The states

| State         | What it means                                                                                       | What can come next                                                  | Settled |
| ------------- | --------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------- | ------- |
| `scheduled`   | Has a `send_at` in the future. You can edit or cancel it.                                           | `queued` at `send_at`, or `canceled`.                               | No      |
| `queued`      | Accepted and waiting for the Mac. `hold` says why when it can’t go yet.                             | `sending`, `expired` or `canceled`.                                 | No      |
| `sending`     | The Mac claimed it and is handing it to Messages.                                                   | `sent`, `failed` or `unconfirmed`.                                  | No      |
| `sent`        | Messages shows it sent. `service` says iMessage or SMS.                                             | `delivered` (iMessage only).                                        | Yes     |
| `delivered`   | iMessage reported it on the person’s device. SMS never reaches this state.                          | `read`, if they share read receipts.                                | Yes     |
| `read`        | The person opened it.                                                                               | Nothing; this is as far as a text goes.                             | Yes     |
| `failed`      | It didn’t go. `failure_reason` says why.                                                            | Nothing. Send a new text if you want to try again.                  | Yes     |
| `unconfirmed` | The Mac handed it to Messages but couldn’t confirm it went. BlueBot won’t send it again on its own. | `sent` when you mark it sent, or a new text when you send it again. | No      |
| `expired`     | Still waiting when `expires_at` passed.                                                             | Nothing.                                                            | Yes     |
| `canceled`    | Canceled while waiting. `cancel_reason` says by whom.                                               | Nothing.                                                            | Yes     |
| `received`    | A text that came in.                                                                                | Nothing.                                                            | Yes     |

"Settled" means the text won't move again on its own, apart from `sent` going on to `delivered` and `read`. `unconfirmed` isn't settled: it waits for you to say what happened.

## Every move and its webhook

| From          | To            | When                                                                                                                                                             | Event                 |
| ------------- | ------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------- |
| (new)         | `queued`      | You send a text that's due now. A `hold` may say why it can't go yet.                                                                                            | `message.queued`      |
| (new)         | `scheduled`   | You send a text with a `send_at` more than 5 seconds ahead.                                                                                                      | None                  |
| (new)         | `canceled`    | The person is on your stop list. `cancel_reason` is `opted_out`.                                                                                                 | `message.canceled`    |
| `scheduled`   | `queued`      | Its `send_at` arrives, or you move `send_at` to now with `PATCH`.                                                                                                | `message.queued`      |
| `scheduled`   | `canceled`    | You cancel it, the person stops, or its sequence or broadcast stops.                                                                                             | `message.canceled`    |
| `queued`      | `sending`     | The Mac claims it and starts handing it to Messages.                                                                                                             | None                  |
| `queued`      | `canceled`    | You cancel it, the person stops, or its sequence or broadcast stops.                                                                                             | `message.canceled`    |
| `queued`      | `expired`     | It's still waiting when `expires_at` passes.                                                                                                                     | `message.expired`     |
| `sending`     | `sent`        | Messages shows it sent.                                                                                                                                          | `message.sent`        |
| `sending`     | `queued`      | An `auto` iMessage failed and never went, so BlueBot queues it once more as SMS (`sms_retry: true`). Or sending was paused before the Mac handed it to Messages. | None                  |
| `sending`     | `failed`      | Messages refused it, or an `imessage` text went to someone not on iMessage.                                                                                      | `message.failed`      |
| `sending`     | `unconfirmed` | The Mac's 3-minute lease ran out with no report.                                                                                                                 | `message.unconfirmed` |
| `unconfirmed` | `sent`        | You mark it sent, the Mac later finds it in Messages, or it's reported delivered.                                                                                | `message.sent`        |
| `sent`        | `delivered`   | iMessage shows it on the person's device.                                                                                                                        | `message.delivered`   |
| `delivered`   | `read`        | The person read it, and they share read receipts.                                                                                                                | `message.read`        |
| (new, in)     | `received`    | Somebody texted you.                                                                                                                                             | `message.received`    |

A text that goes straight from `sent` to `read` fires both `message.delivered` and `message.read`, in that order. Tapbacks, edits and unsends don't change a text's state: they fire `reaction.added`, `reaction.removed`, `message.edited` and `message.unsent`.

The message on every event is the whole message as `GET /v1/messages/{id}` would answer at that moment, with your `metadata`. A handler can act on the event alone.

## When each happened

Every move stamps a time on the message, in UTC: `created_at`, `queued_at`, `sending_at`, `sent_at`, `delivered_at`, `read_at`, `failed_at`, `unconfirmed_at`, `expired_at`, `canceled_at`, and `received_at` for a text in. A stamp is `null` until its move happens. `send_at` and `expires_at` are when a text is due and when it gives up.

## Why a text failed

A `failed` text has `failure.reason`, a code to program against, and `failure.detail`, what the Mac saw in words.

| `failure.reason`   | What happened                                                                                      | What to do                                                                                                           |
| ------------------ | -------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- |
| `messages_refused` | Messages on the Mac refused to send it, or showed an error.                                        | Read `failure.detail`. Often Messages was signed out; check `GET /v1/device`, fix it, then send a new text.          |
| `not_on_imessage`  | It was sent as `service: "imessage"` and the person isn't on iMessage, or an SMS retry failed too. | Send it again with `service: "auto"` or `"sms"`, which goes through your iPhone. [Services](https://usebluebot.com/docs/concepts/services) |
| `file_unavailable` | A file couldn't be sent. On a `sent` text this means the words went and a file didn't.             | Check the file, upload it again and send the file on its own.                                                        |

A failed text is never retried by BlueBot. If you want to try again, send a new text.

## Why a text was canceled

| `cancel_reason`      | Who canceled it                                                                                                             |
| -------------------- | --------------------------------------------------------------------------------------------------------------------------- |
| `requested`          | You, with `POST /v1/messages/{id}/cancel`, the dashboard or a tool.                                                         |
| `opted_out`          | The person is on your stop list, because they texted a stop word or you added them. [Stop words](https://usebluebot.com/docs/concepts/stop-words) |
| `sequence_stopped`   | Its sequence stopped, usually because the person replied.                                                                   |
| `broadcast_canceled` | You canceled the broadcast it was part of.                                                                                  |
| `staff`              | A person at BlueBot canceled it. You'll have heard from them about why.                                                     |

Only a `scheduled` or `queued` text can be canceled. Once the Mac has claimed it (`sending`), it's in Messages' hands and the cancel answers [`conflict`](https://usebluebot.com/docs/errors/conflict). [Change or cancel a text](https://usebluebot.com/docs/sending/change-or-cancel)

## Settle an unconfirmed text

Now and then the Mac hands a text to Messages and can't tell whether it went: Messages didn't show it sent within 30 seconds, or the Mac lost its connection mid-send. BlueBot never guesses and never sends it again on its own, because the person might get it twice. The text becomes `unconfirmed` and waits for you.

Often it settles itself: if the Mac later finds the text in Messages, or iMessage reports it delivered, it becomes `sent` and `message.sent` fires. Otherwise, look at the conversation in Messages and tell BlueBot what happened:

```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: resolve-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: 'resolve-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="resolve-msg_1Hq2LxW9cVbN3mKpRtYs0a",
)
print(message)
```

- `mark_sent` when the text is there in the conversation. It becomes `sent` with `resolution: "marked_sent"`, and `message.sent` fires.
- `send_again` when it isn't. BlueBot queues a new text with the same words, files, service and metadata. The old one stays `unconfirmed` with `resolution: "resent"` and points to the new one in `resent_message_id`.

| You got                                                        | It means                                                 | Do next                                                           |
| -------------------------------------------------------------- | -------------------------------------------------------- | ----------------------------------------------------------------- |
| `200` with `"state": "sent"` and `"resolution": "marked_sent"` | It's settled as sent.                                    | Nothing.                                                          |
| `200` with `"resolution": "resent"` and a `resent_message_id`  | A new text is queued.                                    | Follow the new id, or wait for its `message.sent`.                |
| `409 conflict`                                                 | The text isn't `unconfirmed`, or it was already settled. | Read it with `GET /v1/messages/{id}`. It may have settled itself. |

The dashboard's home page lists every unconfirmed text, and `GET /v1/device` counts them in `unconfirmed_count`.

## Texts you typed yourself

The Mac copies both directions of the conversations it reads. A text you type by hand in Messages on your Mac or iPhone arrives as a message with `direction: "out"`, `state: "sent"` and `source: "mac"`, and fires `message.sent`. It was never queued, so it has no hold, no `metadata` and no `queued_at`. Your handlers can tell it apart by its `source`.

## Watch state without polling

Reading a text back with `GET /v1/messages/{id}` is fine while you test. In an app, a [webhook](https://usebluebot.com/docs/webhooks) for `message.sent`, `message.failed`, `message.expired` and `message.unconfirmed` tells you when anything settles, and costs nothing against your rate limit. If your server was down, `GET /v1/events` has the last 30 days.
