# 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_limit` until midnight. Midnight is inside her quiet hours, so at midnight their hold becomes `quiet_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).

> **Don't retry a held text.** A hold is not an error. Sending the same text again makes a second text, which will also be held, and both will go. Read `hold.reason` if you want to say why it's waiting, or listen for `message.sent`.

## 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                                     | [`invalid_request`](https://usebluebot.com/docs/errors/invalid_request) (400), with `details` naming `text` |
| One file                | 25 MB                                                | [`payload_too_large`](https://usebluebot.com/docs/errors/payload_too_large) (413)                           |
| Files on one text       | 10                                                   | [`invalid_request`](https://usebluebot.com/docs/errors/invalid_request) (400)                               |
| Metadata                | 20 keys, names up to 40 characters, values up to 500 | [`invalid_request`](https://usebluebot.com/docs/errors/invalid_request) (400)                               |
| A JSON body             | 1 MB                                                 | [`payload_too_large`](https://usebluebot.com/docs/errors/payload_too_large) (413)                           |
| Requests per key        | 120 a minute                                         | [`rate_limited`](https://usebluebot.com/docs/errors/rate_limited) (429) with `Retry-After`                  |
| API keys per account    | 10                                                   | [`limit_reached`](https://usebluebot.com/docs/errors/limit_reached) (409)                                   |
| People in one broadcast | 500                                                  | [`limit_reached`](https://usebluebot.com/docs/errors/limit_reached) (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 [webhooks](https://usebluebot.com/docs/webhooks) instead. Every answer carries `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset`, so a program can slow down before it's refused. [Rate limits](https://usebluebot.com/docs/concepts/rate-limits) 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. [Data retention](https://usebluebot.com/docs/concepts/data-retention) |
| 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. [Retries](https://usebluebot.com/docs/webhooks/retries)             |
| A file's `url`                   | An hour                                                | Ask `GET /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

`GET /v1/device` answers today's counts against the daily limits, and how many texts are waiting:

```bash
curl "https://usebluebot.com/api/v1/device" \
  -H "Authorization: Bearer $BLUEBOT_API_KEY"
```

```ts
import BlueBot from '@usebluebot/sdk';

const client = new BlueBot({ apiKey: process.env.BLUEBOT_API_KEY });

const device = await client.device.get();
console.log(device);
```

```python
import os
from bluebot import BlueBot

client = BlueBot(api_key=os.environ["BLUEBOT_API_KEY"])

device = client.device.get()
print(device)
```

The answer, `200`:

```json
{
  "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 [What BlueBot is not](https://usebluebot.com/docs#what-bluebot-is-not) 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.
