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.
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 /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 /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. |
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. |
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 .
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:
/v1/messages/msg_1Hq2LxW9cVbN3mKpRtYs0a/resolvecurl -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"
}'mark_sentwhen the text is there in the conversation. It becomessentwithresolution: "marked_sent", andmessage.sentfires.send_againwhen it isn't. BlueBot queues a new text with the same words, files, service and metadata. The old one staysunconfirmedwithresolution: "resent"and points to the new one inresent_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 /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 /v1/messages/{id} is fine while you test. In an app, a 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, /v1/events has the last 30 days.