Holds
Why a text that can't go yet is accepted and waits instead of failing, every reason it can wait, the order they're checked in, and when each one clears.
A text that can't go right now isn't refused. BlueBot accepts it, answers 201, and puts a hold on it that says why it's waiting and, when it knows, until when. When the hold clears, the text goes by itself. Your code never needs to retry a send because of quiet hours, a sleeping Mac or a daily limit.
What a hold looks like
Every outgoing message has a hold field. It's null when nothing holds the text, and an object when something does:
{
"id": "msg_1Hq2LxW9cVbN3mKpRtYs0a",
"object": "message",
"state": "queued",
"to": "+15555550123",
"hold": {
"reason": "quiet_hours",
"until": "2026-11-14T14:00:00.000Z",
"description": "Waiting for quiet hours to end at 8:00 AM tomorrow."
},
"expires_at": "2026-11-15T04:40:00.000Z"
}| Field | What it is |
|---|---|
reason | Why it waits, as a stable code. Program against this. |
until | When it should stop waiting, when that's known: the end of quiet hours, the next midnight, or the text's estimated slot in the pace. null when nobody can say, such as while the Mac is away. |
description | The hold in words, in your time zone, ready to show a person. |
A hold is only ever on a queued text. A scheduled text has none until it's due.
Every reason
hold.reason | What it means | When it clears |
|---|---|---|
account_held | BlueBot’s operator paused this account’s sending. Nothing sends. | Only BlueBot, after you write to support. Meanwhile the API still accepts sends, and they wait with this hold. |
platform_stopped | BlueBot stopped all sending for every account, for an incident. | BlueBot turning sending back on. Texts go in their order, subject to their expiry. |
plan_lapsed | The account’s plan isn’t active. | An active plan again (update the card on the Billing page). |
paused | You paused sending from the Mac’s menu or the dashboard. | Resuming, or the pause’s own end (until): an hour, or 8:00 AM in your zone. |
device_offline | Your Mac hasn’t checked in for 2 minutes: asleep, off, or off the internet. | The Mac checking in again. If it stays away past the text’s expires_at, the text expires. |
quiet_hours | It’s inside your quiet hours (9:00 PM to 8:00 AM in your zone unless you changed them). | until: the end of quiet hours. The text then takes its place in the pace. |
daily_new_people_limit | You’ve texted as many new people today as your setting allows (50 by default). People you already have a conversation with don’t count. | until: the next midnight in your zone, or raising the setting. |
platform_daily_limit | The account sent 1,000 texts today, BlueBot’s ceiling for one account. | until: the next midnight in your zone. This one isn’t a setting. |
pacing | Waiting its turn: the Mac leaves your pace (20 seconds by default) between texts. | until: its estimated slot. It moves as texts ahead of it go or are canceled. |
The order they're checked
BlueBot checks the reasons in a fixed order and shows the first that applies, so a text has one hold at a time. The first six apply to the whole account, so every queued text shares them:
account_heldplatform_stoppedplan_lapsedpauseddevice_offlinequiet_hours
The last three apply text by text, in the order the texts were queued:
daily_new_people_limit, only for a text to someone you've never texted with through BlueBotplatform_daily_limitpacing
When one hold clears, the next may show. A text queued at 11:00 PM waits on quiet_hours; at 8:00 AM, if twenty texts went in ahead of it, it waits on pacing for a few minutes more, then goes. A text held by a daily limit doesn't use up a place in the pace, so texts behind it that can go still go.
Holds are worked out again every time anything changes: a send, a cancel, a settings change, the Mac checking in or going away. Raise new_people_per_day and the texts waiting on it are ready on the Mac's next check-in.
What a hold doesn't change
- Expiry still runs. A held text that's still waiting at its
expires_at(24 hours after it was due, by default) becomesexpiredand never goes. Chooseexpires_atfor texts that only make sense soon, and longer for ones that can wait. - You can still cancel it. A held text is
queued, and POST/v1/messages/{id}/cancelcancels it. - The stop list isn't a hold. A text to someone on your stop list doesn't wait; it's
canceledat once withcancel_reason: "opted_out".
New sends while the account is held
Most holds let new sends in: they're accepted and wait with everything else. Two refuse new work instead, because nothing will move until a person acts:
- A lapsed plan answers new sends, uploads and webhook creation with (402). Texts already queued wait with
hold.reason: "plan_lapsed"and go once the plan is active again, if they haven't expired. Reading, canceling and changing settings keep working. - BlueBot pausing your account shows as
account_heldon every queued text. Write to ; a person at BlueBot releases it.
Don't retry a held text
The answer to a held send is a success. Sending the same text again makes a second text with its own hold, and both go when the hold clears. If your code sends in a loop, send each text once with an Idempotency-Key, and let the holds do the waiting.
Find what's held
/v1/devicecounts them:held_countis how many queued texts are waiting on a hold, andqueued_countis every queued text.GET /v1/messages?state=queuedlists them, each with itshold.message.queuedfires once when a text is queued and carries the hold it had at that moment. A hold changing doesn't fire an event. Listen formessage.sent,message.expiredandmessage.canceledto learn how each one ends.
/v1/messagescurl "https://usebluebot.com/api/v1/messages?state=queued&limit=50" \
-H "Authorization: Bearer $BLUEBOT_API_KEY"| You got | It means | Do next |
|---|---|---|
Every text with the same hold.reason | An account-wide hold: the Mac is away, it's quiet hours, or sending is paused. | Fix the cause, or wait for hold.until. |
pacing with times spread over the next hour | A long line at your pace. | Nothing; they go in order. Lower pace_seconds only if you understand the risk. |
daily_new_people_limit on some texts | You've texted as many new people today as your setting allows. | Wait for midnight, or raise new_people_per_day. |
device_offline with until: null | The Mac hasn't checked in for 2 minutes. | Wake it, or check its power and network. |
Tell people why
If your app shows what it sent, show hold.description. It's written for a person and set in your account's time zone: "Waiting for quiet hours to end at 8:00 AM tomorrow.", "Waiting for your Mac to come back online.", "Waiting its turn. It should go around 2:41 PM." The dashboard, the Mac's menu and BlueBot's MCP tools all use the same words.