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.
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:
/v1/messages/msg_1Hq2LxW9cVbN3mKpRtYs0acurl -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"
}'| 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:
/v1/messages/msg_1Hq2LxW9cVbN3mKpRtYs0a/cancelcurl -X POST "https://usebluebot.com/api/v1/messages/msg_1Hq2LxW9cVbN3mKpRtYs0a/cancel" \
-H "Authorization: Bearer $BLUEBOT_API_KEY" \
-H "Idempotency-Key: cancel-msg_1Hq2LxW9cVbN3mKpRtYs0a"{
"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.
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:
- Look at the conversation in Messages on your Mac or iPhone.
- If the text is there and shows as sent, mark it sent.
- If it isn’t, or it shows “Not Delivered”, send it again.
/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: settle-msg_1Hq2LxW9cVbN3mKpRtYs0a" \
-d '{
"action": "mark_sent"
}'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. |
/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: settle-msg_1Hq2LxW9cVbN3mKpRtYs0a-again" \
-d '{
"action": "send_again"
}'{
"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:
/v1/messagescurl "https://usebluebot.com/api/v1/messages?state=unconfirmed" \
-H "Authorization: Bearer $BLUEBOT_API_KEY"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.