Limits
Every cap BlueBot has, why it's there and what happens when you reach it. Most of them hold a text until it can go rather than refuse it.
BlueBot sends from your own number through your own Mac, so its limits are there to keep that number in good standing with Apple and the carriers, and to keep one runaway loop from becoming a problem. They come in two kinds:
- Sending rules are yours. Pace, new people per day, quiet hours and expiry each have a default you can change in Settings → Sending rules or with PATCH
/v1/settings. - Fixed limits are the same for every account: the daily ceiling, sizes, the API's rate limit and how long things are kept.
Almost nothing here refuses a send. A text that can't go yet is accepted and waits with a hold that says why and until when, then goes on its own. The few limits that do refuse a request answer with an error code before anything is queued, so nothing is half done.
Every limit
| Limit | Value | Why | When you reach it | A setting? |
|---|---|---|---|---|
| Texts per day | 1,000 per account | One person’s business never needs more, and a number that sends more than this looks like a bulk sender to Apple. | Texts over it wait with platform_daily_limit until midnight in your zone, and you get one email. | No |
| New people per day | 50 by default | Apple flags accounts that message many strangers. Texts to people already in a conversation don’t count. | Texts to new people wait with daily_new_people_limit until midnight. Raise it in Settings; the dashboard warns above 200. | Yes |
| Pace | 20 seconds between texts (5 to 300) | A person types one text at a time; a Mac that fires 50 in a second looks like a bot. | Texts queue in order with pacing and an estimated until. | Yes |
| Quiet hours | 9:00 PM to 8:00 AM in your zone, on by default | Texts at 3 AM earn STOP replies. | Texts wait with quiet_hours until they end. | Yes |
| Expiry | 24 hours by default; any time you set per text | “Your table is ready” is wrong a day later. A text that can’t go in time shouldn’t go late. | A text still waiting at expires_at becomes expired and fires message.expired. | Yes |
| Text length | 2,000 characters | Long texts are split by carriers when they fall back to SMS, and read badly as iMessage. | The send is refused with invalid_request. | No |
| Files | 25 MB each, 10 per text | Messages handles big files slowly, and SMS can’t carry them at all. | The upload is refused with payload_too_large. | No |
| Metadata | 20 keys, 500 characters a value | It’s sent with every event for the text, so it’s meant for ids rather than documents. | The send is refused with invalid_request. | No |
| API requests | 120 a minute per key | Keeps one runaway loop from slowing everyone. Sends that can’t go yet are held, never refused with a 429. | rate_limited (429) with retry_after. Every answer carries RateLimit-* headers. | No |
| Idempotency keys | kept 24 hours | That covers any retry without holding a key forever. | After 24 hours the same key is treated as new. | No |
| API keys | 10 live per account | One per app or person is plenty; more is usually keys nobody revoked. | limit_reached until you revoke one. | No |
| Broadcast size | 500 recipients | Broadcasts are for people who asked to hear from you, and the Mac sends them at your pace. | The broadcast is refused with limit_reached. | No |
| Words kept | 30 days | The words of a text are the private part, so BlueBot deletes them first. | 30 days after a text went or came, its text and files are deleted, as are a finished broadcast’s words; the facts (who, when, state) stay. | No |
| Events kept | 30 days | Long enough to catch up after an outage on your side. | GET /v1/events reads back 30 days. | No |
| Webhook retries | about 72 hours | Covers a weekend outage on your side. | After 72 hours of failures the webhook is paused and you get one email. Nothing is dropped silently. | No |
| Lookups | answered within 15 seconds | The answer comes from your Mac, which may be busy or away. | After 15 seconds the answer is service: "unknown" with basis: "timeout" or "device_offline". | No |
Limits that hold a text
These four never cause an error. The text is answered 201, comes back queued with a hold, and goes by itself when the hold clears, unless its expires_at passes first.
| Limit | hold.reason | It clears |
|---|---|---|
| Texts per day (1,000) | platform_daily_limit | At midnight in your time zone. |
| New people per day (50 by default) | daily_new_people_limit | At midnight in your time zone, or when you raise the setting. |
| Pace (20 seconds by default) | pacing | At the text's slot. hold.until is the estimate. |
| Quiet hours (9:00 PM to 8:00 AM by default) | quiet_hours | When quiet hours end. |
Here is how they add up. Dana runs a small cleaning company and sends 120 texts at 2:00 PM to remind clients of next week's visits. 80 of the clients have never texted with her through BlueBot.
- Only 50 of the 80 new people can be texted today. The other 30 wait with
daily_new_people_limituntil midnight. Midnight is inside her quiet hours, so at midnight their hold becomesquiet_hours, and they go from 8:00 AM. - The 90 that can go today leave her Mac 20 seconds apart, so the last of them goes about 30 minutes after the first. While they wait their turn, their hold is
pacing. - The 30 held overnight still have until 2:00 PM tomorrow before their 24-hour expiry, so they go in time.
- None of this needs a retry. Her loop sent 120 requests, got 120 answers and is done.
A text to someone you already have a conversation with never counts against new people, so a reply to a client who texted you a minute ago goes at once (after the pace).
Limits that refuse a request
These answer with an error and queue nothing. Fix the request and send it again.
| Limit | Value | You get |
|---|---|---|
| Words in one text | 2,000 characters | (400), with details naming text |
| One file | 25 MB | (413) |
| Files on one text | 10 | (400) |
| Metadata | 20 keys, names up to 40 characters, values up to 500 | (400) |
| A JSON body | 1 MB | (413) |
| Requests per key | 120 a minute | (429) with Retry-After |
| API keys per account | 10 | (409) |
| People in one broadcast | 500 | (409) |
The rate limit counts requests, not texts. A send that goes through but is held still counts as one request, and reading a message back counts as one too. If you poll for state, poll slowly or use instead. Every answer carries RateLimit-Limit, RateLimit-Remaining and RateLimit-Reset, so a program can slow down before it's refused. has the details.
Limits on time
| What | How long | What happens after |
|---|---|---|
| A queued text | 24 hours after it was due, unless you set expires_at | It becomes expired and message.expired fires. It never goes. |
An Idempotency-Key | 24 hours | The same key is treated as new. |
| The words and files of a text | 30 days | They're deleted. The message's facts stay. |
Events (GET /v1/events) | 30 days | Older events are gone from the feed. |
| Webhook retries | About 72 hours | The webhook is paused and you get one email. |
A file's url | An hour | Ask /v1/files/{id} for a fresh one. |
| A lookup | Up to 15 seconds | The answer is service: "unknown" with basis: "timeout". |
| A file you upload and never send | A day | It's deleted. |
The expiry setting runs from 1 to 168 hours (a week). Per text, expires_at can be any time after the text is due.
See where you stand
/v1/device answers today's counts against the daily limits, and how many texts are waiting:
/v1/devicecurl "https://usebluebot.com/api/v1/device" \
-H "Authorization: Bearer $BLUEBOT_API_KEY"{
"object": "device",
"status": "online",
"held_count": 2,
"queued_count": 5,
"today": {
"sent": 14,
"new_people": 3,
"new_people_limit": 50,
"daily_limit": 1000,
"resets_at": "2026-11-14T05:00:00.000Z"
}
}| You got | It means | Do next |
|---|---|---|
today.new_people equal to today.new_people_limit | Texts to anyone new wait until resets_at. Texts to people you already talk with still go. | Wait, or raise new_people_per_day if you know these people asked to hear from you. |
today.sent equal to today.daily_limit | Every text waits until resets_at. You got one email about it. | Wait. The ceiling isn't a setting. |
held_count above 0 | Some queued texts are waiting on a hold. | List them with GET /v1/messages?state=queued and read each hold. |
resets_at is the next midnight in your account's time zone, written in UTC. Today's counts start again then.
Why the daily ceiling isn't a setting
1,000 texts a day is far more than one person's business sends, and a number that sends more than that looks like a bulk sender to Apple, whatever the reason. BlueBot is for texting people who know you. If you need to text thousands of people a day, a hosted service with its own numbers is the better tool, as explains.
The new-people setting is yours, from 0 to 1,000. The dashboard warns above 100, because Apple flags accounts that message many strangers, and once an account is flagged no setting brings it back.