# Quickstart

> Make an API key and text yourself from your own number in four steps, then learn the five things to know before you send in a loop.

This takes about five minutes. You need a BlueBot account with your Mac connected. If you haven't connected it yet, open **Connect your Mac** in the dashboard first; the app walks you through it.

Every example below shows curl, the TypeScript and Python SDKs, Go, Ruby, PHP and plain `fetch`. Pick a language once and every example on every page follows it.

### Make an API key

In the dashboard, open **Developers → API keys** and press **New key**. Name it after what will use it ("Quickstart" will do), keep the scopes `messages:send` and `messages:read` ticked, and copy the key. It starts with `bb_live_` and is shown once.

Put it in your environment so the examples can read it:

```bash
export BLUEBOT_API_KEY="bb_live_…"
```

Check it with `GET /v1/me`, which says which account and key a request acts as:

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

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

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

const me = await client.me();
console.log(me);
```

```python
import os
from bluebot import BlueBot

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

me = client.me()
print(me)
```

| You got                         | It means                                                                        | Do next                                                                          |
| ------------------------------- | ------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- |
| `200` with your account's email | The key works.                                                                  | Go on to step 2.                                                                 |
| `401 unauthorized`              | BlueBot doesn't know the key: mistyped, revoked, or not sent as `Bearer <key>`. | Copy the key again, or make a new one. [unauthorized](https://usebluebot.com/docs/errors/unauthorized) |
| `403 insufficient_scope`        | The key can't read the account.                                                 | Tick `account:read` on the key, or skip this check.                              |

### Check your Mac

BlueBot sends through your Mac, so make sure it's there:

```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)
```

| You got                                      | It means                                                                   | Do next                                                                      |
| -------------------------------------------- | -------------------------------------------------------------------------- | ---------------------------------------------------------------------------- |
| `"status": "online"` and `"checks_ok": true` | The Mac checked in within the last 2 minutes and every check passed.       | Go on to step 3.                                                             |
| `"status": "offline"`                        | The Mac hasn't checked in for 2 minutes: asleep, off, or off the internet. | Wake it. Texts you send now wait for it rather than fail.                    |
| `"status": "never_connected"`                | No Mac is connected to this account.                                       | Open **Connect your Mac** in the dashboard.                                  |
| `"checks_ok": false`                         | A setup check needs you, such as Messages being signed out.                | Open BlueBot from the Mac's menu bar; it says which check and how to fix it. |

### Text yourself

Send a text to your own phone number. It arrives in Messages on your iPhone like any other text you send yourself.

```bash
curl -X POST "https://usebluebot.com/api/v1/messages" \
  -H "Authorization: Bearer $BLUEBOT_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: quickstart-first-text" \
  -d '{
    "to": "+15555550123",
    "text": "Hello from BlueBot",
    "metadata": {
      "test": "quickstart"
    }
  }'
```

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

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

const message = await client.messages.send({
  to: '+15555550123',
  text: 'Hello from BlueBot',
  metadata: {
    test: 'quickstart',
  },
}, { idempotencyKey: 'quickstart-first-text' });
console.log(message);
```

```python
import os
from bluebot import BlueBot

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

message = client.messages.send(
    to="+15555550123",
    text="Hello from BlueBot",
    metadata={
        "test": "quickstart",
    },
    idempotency_key="quickstart-first-text",
)
print(message)
```

Put your own number in `to`. US numbers can leave off the `+1`, and `(555) 555-0123` works too.

| You got                                           | It means                                                                                             | Do next                                                                                                                                                             |
| ------------------------------------------------- | ---------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `201` with `"state": "queued"` and `"hold": null` | BlueBot accepted the text and it's next in line. It goes within a few seconds.                       | Copy the message's `id` and go on to step 4.                                                                                                                        |
| `201` with a `hold`                               | BlueBot accepted the text and it's waiting. `hold.reason` says why and `hold.until` says until when. | Read [Holds](https://usebluebot.com/docs/concepts/holds). A test at night usually meets `quiet_hours`.                                                                                    |
| `201` with `"state": "canceled"`                  | Your number is on your own stop list.                                                                | Take it off with `DELETE /v1/stops/{person}` and send again.                                                                                                        |
| `400 invalid_request`                             | A field is wrong. `error.details` names it.                                                          | Fix the field and send again with a new `Idempotency-Key`. The old key keeps its first answer, and a changed request under it is refused as `idempotency_conflict`. |

A text that can't go right now is never refused. It's accepted with a `hold` and goes on its own when the hold ends, so a loop never needs to retry a send.

### Watch it go

Read the message back with the `id` from step 3:

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

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

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

const message = await client.messages.get('msg_1Hq2LxW9cVbN3mKpRtYs0a');
console.log(message);
```

```python
import os
from bluebot import BlueBot

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

message = client.messages.get("msg_1Hq2LxW9cVbN3mKpRtYs0a")
print(message)
```

| You got                                       | It means                                                    | Do next                                                                 |
| --------------------------------------------- | ----------------------------------------------------------- | ----------------------------------------------------------------------- |
| `"state": "sent"` and `"service": "imessage"` | Messages on your Mac shows it sent as an iMessage.          | Look at your phone. Soon `delivered_at` fills in too.                   |
| `"state": "sent"` and `"service": "sms"`      | It went as a regular text through your iPhone.              | SMS never reports delivery, so `sent` is as far as it goes.             |
| `"state": "queued"` or `"sending"`            | It hasn't gone yet.                                         | Read it again in a few seconds, or check `hold`.                        |
| `"state": "failed"`                           | Messages couldn't send it. `failure.reason` says why.       | Read [Message states](https://usebluebot.com/docs/concepts/message-states).                   |
| `"state": "unconfirmed"`                      | The Mac handed it to Messages and couldn't confirm it went. | Look in Messages, then settle it with `POST /v1/messages/{id}/resolve`. |

Polling is fine for a test. In an app, let BlueBot tell you instead: point a [webhook](https://usebluebot.com/docs/webhooks) at your server, or run `bluebot listen --forward-to http://localhost:3000/webhooks` while you build.

## Before you loop

You've sent one text. Before you send a hundred from a loop, a Zap or an agent, know these five things.

### Holds

A send is answered at once, and a text that can't go yet comes back `queued` with a `hold`. It goes by itself when the hold clears. Don't retry a held text and don't treat a hold as an error; read `hold.reason` if you want to tell someone why it's waiting. [Holds](https://usebluebot.com/docs/concepts/holds) lists every reason.

The answer, `201`:

```json
{
  "id": "msg_2Bf8QzD4kJmA6nXwEuTr1c",
  "object": "message",
  "state": "queued",
  "to": "+15555550123",
  "hold": {
    "reason": "quiet_hours",
    "until": "2026-11-14T13:00:00.000Z",
    "description": "Waiting for quiet hours to end at 8:00 AM."
  }
}
```

### Pacing

The Mac leaves 20 seconds between texts by default, the way a person sends them one at a time. Send 30 texts in a loop and the 30th goes about 10 minutes later. You can set the pace from 5 to 300 seconds; a faster pace is the quickest way to get a number flagged. [Sending rules](https://usebluebot.com/docs/concepts/sending-rules)

### Quiet hours

From 9:00 PM to 8:00 AM in your time zone, texts wait. That's on by default and you can change the hours. A text sent at 11 PM goes at 8 AM, unless it expires first.

### Idempotency

Send an `Idempotency-Key` header with every POST: any string that is new for each text you mean to send, such as your own order id. If your request times out and you send it again with the same key, you get the first answer back and nobody gets two texts. The SDKs make a key for you. [Idempotency](https://usebluebot.com/docs/concepts/idempotency)

### Expiry

A text still waiting 24 hours after it was due expires and never goes, so "Your table is ready" can't arrive a day late. Set `expires_at` on a send to choose a shorter or longer wait, and listen for `message.expired`. [Scheduled sends](https://usebluebot.com/docs/sending/scheduled)

## Next

- [Receive replies](https://usebluebot.com/docs/webhooks): a webhook for `message.received`.
- [Send a picture](https://usebluebot.com/docs/sending/files): upload a file and send it with the words.
- [Install an SDK](https://usebluebot.com/docs/sdks/typescript): retries, idempotency keys and paging are built in.
- [Limits](https://usebluebot.com/docs/limits): every cap, why it's there, and what happens when you reach it.
