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:
export BLUEBOT_API_KEY="bb_live_…"Check it with GET /v1/me, which says which account and key a request acts as:
/v1/mecurl "https://usebluebot.com/api/v1/me" \
-H "Authorization: Bearer $BLUEBOT_API_KEY"| 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. |
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:
/v1/devicecurl "https://usebluebot.com/api/v1/device" \
-H "Authorization: Bearer $BLUEBOT_API_KEY"| 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.
/v1/messagescurl -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"
}
}'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 . 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:
/v1/messages/msg_1Hq2LxW9cVbN3mKpRtYs0acurl "https://usebluebot.com/api/v1/messages/msg_1Hq2LxW9cVbN3mKpRtYs0a" \
-H "Authorization: Bearer $BLUEBOT_API_KEY"| 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 . |
"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 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. lists every reason.
{
"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.
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.
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.
Next
- : a webhook for
message.received. - : upload a file and send it with the words.
- : retries, idempotency keys and paging are built in.
- : every cap, why it's there, and what happens when you reach it.