Skip to the page

Get started

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:

Terminal
export BLUEBOT_API_KEY="bb_live_…"

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

GET/v1/meWho am I? →
curl "https://usebluebot.com/api/v1/me" \
  -H "Authorization: Bearer $BLUEBOT_API_KEY"
You gotIt meansDo next
200 with your account's emailThe key works.Go on to step 2.
401 unauthorizedBlueBot doesn't know the key: mistyped, revoked, or not sent as Bearer <key>.Copy the key again, or make a new one. unauthorized
403 insufficient_scopeThe 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:

GET/v1/deviceGet the Mac →
curl "https://usebluebot.com/api/v1/device" \
  -H "Authorization: Bearer $BLUEBOT_API_KEY"
You gotIt meansDo next
"status": "online" and "checks_ok": trueThe 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": falseA 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.

POST/v1/messagesSend a text →
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"
    }
  }'

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

You gotIt meansDo next
201 with "state": "queued" and "hold": nullBlueBot 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 holdBlueBot accepted the text and it's waiting. hold.reason says why and hold.until says until when.Read 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_requestA 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:

GET/v1/messages/msg_1Hq2LxW9cVbN3mKpRtYs0aGet a text →
curl "https://usebluebot.com/api/v1/messages/msg_1Hq2LxW9cVbN3mKpRtYs0a" \
  -H "Authorization: Bearer $BLUEBOT_API_KEY"
You gotIt meansDo 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.
"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 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 lists every reason.

Response201A text held for quiet hours
{
  "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

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

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

Next

  • Receive replies: a webhook for message.received.
  • Send a picture: upload a file and send it with the words.
  • Install an SDK: retries, idempotency keys and paging are built in.
  • Limits: every cap, why it's there, and what happens when you reach it.