Skip to the page

Concepts

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.

Message statesA text goes from scheduled to queued to sending to sent, then delivered and read for iMessage. A scheduled or queued text can be canceled; a queued text can expire; a sending text can fail or become unconfirmed. An unconfirmed text is settled by marking it sent or sending it again. Texts that come in are received.scheduledqueuedsendingsentdeliveredreadcanceledexpiredfailedunconfirmedreceivedexpires_at passedmark_sentsend_again: a new texttexts that come inDashed: still waiting. Blue: it went. Red: it won’t go. Orange: it needs you.

The states

StateWhat it meansWhat can come nextSettled
scheduledHas a send_at in the future. You can edit or cancel it.queued at send_at, or canceled.No
queuedAccepted and waiting for the Mac. hold says why when it can’t go yet.sending, expired or canceled.No
sendingThe Mac claimed it and is handing it to Messages.sent, failed or unconfirmed.No
sentMessages shows it sent. service says iMessage or SMS.delivered (iMessage only).Yes
deliverediMessage reported it on the person’s device. SMS never reaches this state.read, if they share read receipts.Yes
readThe person opened it.Nothing; this is as far as a text goes.Yes
failedIt didn’t go. failure_reason says why.Nothing. Send a new text if you want to try again.Yes
unconfirmedThe 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
expiredStill waiting when expires_at passed.Nothing.Yes
canceledCanceled while waiting. cancel_reason says by whom.Nothing.Yes
receivedA 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

FromToWhenEvent
(new)queuedYou send a text that's due now. A hold may say why it can't go yet.message.queued
(new)scheduledYou send a text with a send_at more than 5 seconds ahead.None
(new)canceledThe person is on your stop list. cancel_reason is opted_out.message.canceled
scheduledqueuedIts send_at arrives, or you move send_at to now with PATCH.message.queued
scheduledcanceledYou cancel it, the person stops, or its sequence or broadcast stops.message.canceled
queuedsendingThe Mac claims it and starts handing it to Messages.None
queuedcanceledYou cancel it, the person stops, or its sequence or broadcast stops.message.canceled
queuedexpiredIt's still waiting when expires_at passes.message.expired
sendingsentMessages shows it sent.message.sent
sendingqueuedAn 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
sendingfailedMessages refused it, or an imessage text went to someone not on iMessage.message.failed
sendingunconfirmedThe Mac's 3-minute lease ran out with no report.message.unconfirmed
unconfirmedsentYou mark it sent, the Mac later finds it in Messages, or it's reported delivered.message.sent
sentdeliverediMessage shows it on the person's device.message.delivered
deliveredreadThe person read it, and they share read receipts.message.read
(new, in)receivedSomebody 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.reasonWhat happenedWhat to do
messages_refusedMessages 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_imessageIt 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
file_unavailableA 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_reasonWho canceled it
requestedYou, with POST/v1/messages/{id}/cancel, the dashboard or a tool.
opted_outThe person is on your stop list, because they texted a stop word or you added them. Stop words
sequence_stoppedIts sequence stopped, usually because the person replied.
broadcast_canceledYou canceled the broadcast it was part of.
staffA 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. Change or cancel a text

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:

POST/v1/messages/msg_1Hq2LxW9cVbN3mKpRtYs0a/resolveSettle an unconfirmed text →
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"
  }'
  • 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 gotIt meansDo next
200 with "state": "sent" and "resolution": "marked_sent"It's settled as sent.Nothing.
200 with "resolution": "resent" and a resent_message_idA new text is queued.Follow the new id, or wait for its message.sent.
409 conflictThe 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 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.