CLI
The bluebot command sends and reads texts, checks your Mac, manages webhooks and forwards events to a handler on your laptop with no tunnel. Every command, flag, setting and exit code.
The BlueBot CLI is the bluebot command. It does from a terminal what the dashboard and the API do: send a text, read your texts, check that your Mac is ready, manage webhooks and stops. Its most useful command is bluebot listen, which forwards your account’s events to a webhook handler running on your own machine, signed the same way as a real delivery, so you can build the handler without a tunnel.
Install
The package is @usebluebot/cli; the command it installs is bluebot. It needs Node 18 or newer.
npm install --global @usebluebot/cli
bluebot --versionTo run it once without installing, use npx @usebluebot/cli <command>. The plain npm name bluebot belongs to somebody else, so install the scoped package.
Sign in
bluebot loginlogin asks for an API key and checks it with GET /v1/me. Make the key in the dashboard under Developers → API keys, named “CLI”; tick every scope to use every command, or only the ones you need (listen needs messages:read, the webhooks commands need webhooks:manage, status and whoami need account:read). The key is typed hidden and stored in your system’s keychain: the macOS Keychain, Windows Credential Manager, or the Secret Service on Linux. Where there’s no keychain, such as a container, it goes in a credentials.json file in the config folder that only you can read (mode 0600).
When your terminal shows colour, login greets you with BlueBot drawn in dots. With --json, NO_COLOR or no terminal, it prints plain text.
| Flag | What it does |
|---|---|
--stdin | Read the key from standard input instead of asking, for scripts: printf %s "$KEY" | bluebot login --stdin. |
--profile <name> | Store it under this profile. Default default. |
Use profiles for more than one account or key, such as --profile work. bluebot logout forgets a profile’s key (--all forgets every one); revoke the key itself in the dashboard.
bluebot whoamiSigned in to maria@sparkleclean.com (acct_1Hq2LxW9cVbN3mKpRtYs0a)
Key CLI (bb_live_7Fh2…), live
Scopes messages:send, messages:read, contacts:read, contacts:write, webhooks:manage, account:read
Profile defaultGlobal flags
Every command takes these.
| Flag | What it does |
|---|---|
--json | Print BlueBot’s answer as JSON, exactly as the API returns it (snake_case), with no decoration. |
--api-key <key> | Use this key for one command instead of the stored one. |
--profile <name> | Use this profile’s key. |
--base-url <url> | Talk to another BlueBot, such as one running locally (http://localhost:3100/api/v1). |
--no-color | No colour, as NO_COLOR does. |
-y, --yes | Don’t ask before deleting or replacing something. Without a terminal, commands that would ask refuse unless this is set. |
-h, --help | Help for the command. |
-v, --version | The CLI’s version. |
Send a text
bluebot send +15555550123 "Your table for four is ready."→ +1 555-555-0123 queued msg_1Hq2LxW9cVbN3mKpRtYs0abluebot send <to> [text]. <to> is a phone number in any format (US numbers can leave off +1) or an email address the person uses for iMessage. Leave out the text, or write -, to read it from standard input: fortune | bluebot send +15555550123 -.
| Flag | What it does |
|---|---|
--file <path> | Attach a file, up to 25 MB. Repeat for up to 10. Each is uploaded first. |
--at <time> | Schedule it. An ISO 8601 time (2026-11-20T09:00, read in your account’s time zone when it has no zone) or a time from now (+10m, +2h, +1d). |
--expires <time> | Give up if it’s still waiting then. Same formats. Default: 24 hours after it’s due. |
--service <auto|imessage|sms> | Default auto. |
--metadata <key=value> | Add a metadata key. Repeat for more, up to 20. |
--chat <chat_id> | Text an existing group chat instead of one person; leave out <to>. |
--template <tpl_id> | Send one of your templates instead of words; leave out the text. bluebot templates lists them. |
--var <key=value> | A value for one of the template’s variables, such as --var showing="412 Oak St". Repeat for more. A variable you leave out comes from the contact or your business name, else from the template’s own fallback; one with none of those stops the send, naming it. |
--idempotency-key <key> | Your own idempotency key. By default the CLI makes one per command, so re-running a command sends again. |
--wait | Wait until the text settles: sent, failed, expired, canceled or unconfirmed. A held text is waited for too, up to 10 minutes; if it’s still waiting then, the command prints its hold and exits 0, and the text goes when the hold ends. |
Sent texts show in blue for iMessage and green for SMS once the service is known. A text that can’t go yet prints its hold:
→ +1 555-555-0123 queued, held: quiet hours until 8:00 AM msg_1Hq2LxW9cVbN3mKpRtYs0aTexts from the CLI show source: "cli" in BlueBot.
Read your texts
bluebot messages
bluebot messages --direction in --since 24h
bluebot messages --followbluebot messages (or bluebot messages list) prints your newest texts as a table: time, direction, the person, service, state and the first words.
| Flag | What it does |
|---|---|
--state <state> | Only texts in this state, such as failed. |
--direction <in|out> | Only texts in or out. |
--person <number or email> | Only texts with this person. |
--chat <chat_id> | Only texts in this group chat. |
--source <source> | Only texts from this source, such as api or mac. |
--since <time>, --until <time> | An ISO 8601 time, or a time ago (30m, 24h, 7d). |
--limit <n> | How many, 1 to 100. Default 20. |
--all | Every matching text, following the cursor page after page. |
--follow | Keep running and print each text as it comes in or goes out. Press Ctrl-C to stop. |
--follow reads your account’s event feed every two seconds, so it shows texts typed by hand on your Mac or iPhone too, as BlueBot copies them.
The other messages commands:
| Command | What it does |
|---|---|
bluebot messages get <id> | One text, in full: its times, hold, failure and files. |
bluebot messages edit <id> [--text <words>] [--at <time>] [--expires <time>] | Change a scheduled text before it’s due. |
bluebot messages cancel <id> | Cancel a scheduled or queued text. |
bluebot messages resolve <id> --mark-sent | Settle an unconfirmed text that you checked went. |
bluebot messages resolve <id> --send-again | Settle an unconfirmed text by sending it again as a new text. |
Check your Mac
bluebot statusMac online, MacBook Air (BlueBot 1.4.2, macOS 15.1), last check-in 6 seconds ago
Checks Messages ok, Full Disk Access ok, Text Message Forwarding ok, Awake ok, FileVault ok
Today 41 sent, 6 of 50 new people, daily limit 1,000, resets at 12:00 AM
Queue 3 queued, 1 held, 0 unconfirmed
BlueBot sending normallystatus reads GET /v1/device and GET /v1/status. A check that needs you is shown in yellow with what the Mac saw and what to do. It exits 0 either way; add --json and read checks_ok to use it in a script.
Look up a number
bluebot lookup +15555550123+1 555-555-0123 imessage (from your history, last text 2 days ago)It reads your own Messages history through your Mac and waits up to 15 seconds. A number you’ve never texted is unknown, because Apple gives no way to ask.
Files, templates, conversations, chats, contacts, stops, settings and events
| Command | What it does |
|---|---|
bluebot files upload <path> [--name <name>] | Upload a file and print its file_ id, to send later with the API. |
bluebot files get <id> [--download <path>] | A file’s details; --download saves its bytes. |
bluebot templates [--search <words>] | Your templates, with the variables each one takes. |
bluebot templates get <id> | One template, with its words. |
bluebot conversations [--search <words>] [--filter <all|unread|waiting|archived>] | Your conversations, newest first, as the inbox shows them. waiting means the last text is theirs. all, the default, leaves out archived ones. |
bluebot conversations get <id> | One conversation and its newest text. |
bluebot chats [--groups] [--person <number>] | Your chats, newest first; --groups for group chats only. |
bluebot contacts [--search <words>] [--tag <tag>] [--stopped] | The people you text; --search matches a name, company, number or email. |
bluebot contacts get <id> | One contact. |
bluebot contacts add <number or email> [--first-name] [--last-name] [--company] [--tag <tag>] [--field <key=value>] | Add somebody with their details ahead of time. Repeat --tag and --field for more. A number that’s already a contact is refused; change it with update. |
bluebot contacts update <id> [--first-name] [--last-name] [--company] [--tags <a,b>] [--field <key=value>] | Change a contact. --tags replaces all its tags. |
bluebot contacts delete <id> | Delete a contact. Asks first. |
bluebot stops | Your stop list. |
bluebot stops add <number or email> | Stop texting somebody. Their queued texts are canceled. |
bluebot stops remove <number or email> | Take somebody off the stop list. |
bluebot settings | Your sending rules. |
bluebot settings set <key>=<value> … | Change them, such as pace_seconds=30 quiet_hours.start=20:00 time_zone=America/Chicago. |
bluebot pause [--for <duration> | --until <time>] | Pause all sending, by default for an hour. Texts wait and go when you resume. |
bluebot resume | Resume sending now. |
bluebot events [--type <types>] [--since <time>] | The event feed, newest first; --type takes comma-separated types. |
bluebot events get <id> | One event and its payload. |
Webhooks
| Command | What it does |
|---|---|
bluebot webhooks | Your webhooks, with their status and last answer. |
bluebot webhooks create <url> --events <types> [--description <words>] | Make one. Prints its signing secret once. --events '*' for every type. |
bluebot webhooks get <id> | One webhook. |
bluebot webhooks update <id> [--url] [--events] [--description] [--pause | --resume] | Change one. Resuming sends what waited from the last 72 hours. |
bluebot webhooks delete <id> | Delete one. Asks first. |
bluebot webhooks test <id> [--type <type>] | Have BlueBot send an example event to it, signed with its real secret. Default type message.received. |
bluebot webhooks rotate-secret <id> | A new secret, printed once. The old one keeps signing beside it for 24 hours. Asks first. |
bluebot webhooks deliveries <id> [--state <state>] | Its deliveries: the event, the tries, the last status and error. |
bluebot webhooks replay <id> <delivery_id> | Send one delivery again. |
bluebot webhooks trigger <type> --forward-to <url> | Post an example of an event type to a local URL, signed with your local secret. It needs no key and no network. |
bluebot webhooks trigger <type> --webhook <id> | The same as webhooks test <id> --type <type>. |
Listen for events on your machine
A real webhook needs a public https address. While you build the handler, listen stands in for one: it reads your account’s event feed and posts each event to a URL on your machine, http://localhost included.
bluebot listen --forward-to http://localhost:3000/bluebot/webhooksReady. Forwarding events to http://localhost:3000/bluebot/webhooks
Your local signing secret is whsec_c2VjcmV0LWZvci1sb2NhbC10ZXN0aW5nLW9ubHk=
Set it as BLUEBOT_WEBHOOK_SECRET in your handler. Press Ctrl-C to stop.
19:44:13 message.received evt_7Kd3WnP9xQ2mLc5RvT8bHy → 204 (38 ms)
19:44:31 message.sent evt_2Mx8TqR4vB9nKd1WcY6pLs → 204 (12 ms)
19:45:02 message.delivered evt_9Pq4LmZ7wC3xNb6TkR1vYs → 500 (210 ms)How it works:
- It starts from now, or from
--since, and asksGET /v1/eventsfor new events every two seconds, which uses 30 of your key’s 120 requests a minute. - It posts each event to
--forward-to, oldest first, with the same body and the same three headers a real delivery has (webhook-id,webhook-timestamp,webhook-signature), signed with a secret that belongs to your machine. The CLI makes that secret the first time and keeps it with your key, so it stays the same between runs; set it asBLUEBOT_WEBHOOK_SECRET, and the same verification code works locally and in production. - It prints each event and what your handler answered. It doesn’t retry: fix the handler and use
--sinceto run the events through again. - It reads events only. It doesn’t make a webhook in your account, and your real webhooks keep receiving events as usual.
| Flag | What it does |
|---|---|
--forward-to <url> | Where to post. Required unless --print-json is set. |
--events <types> | Only these types, comma-separated, such as message.received,message.failed. |
--since <time> | Start from an earlier time (10m, 2h, or ISO 8601) and forward what happened since, then carry on. Events are kept 30 days. |
--interval <seconds> | How often to read the feed, 1 to 30. Default 2. |
--print-json | Print each event as one line of JSON as well, or instead when there’s no --forward-to. |
--print-secret | Print the local secret and exit: export BLUEBOT_WEBHOOK_SECRET=$(bluebot listen --print-secret). |
Test events (from webhooks test) aren’t in the event feed, so listen never sees them. To try a handler on an event that hasn’t happened, run listen in one terminal and, in another, bluebot webhooks trigger message.failed --forward-to http://localhost:3000/bluebot/webhooks. covers this and tunnels.
Open the docs or the dashboard
bluebot open docs opens these docs in your browser, and bluebot open dashboard opens the dashboard.
Shell completion
bluebot completion <bash|zsh|fish> prints a completion script. Add it to your shell, for example bluebot completion zsh > "${fpath[1]}/_bluebot" for zsh.
Output and scripting
In a terminal, commands print tables and short lines. With --json, they print the API’s JSON and nothing else, so you can pipe it to jq:
bluebot messages --state failed --all --json | jq -r '.[] | [.to, .failure.reason] | @tsv'- A single record prints as the object the API returns.
- A list prints the page as the API returns it (
object,data,has_more,next_cursor); with--all, an array of every item. listen --print-jsonandmessages --follow --jsonprint one JSON object per line.- Errors go to standard error. With
--json, that’s BlueBot’s error body,{ "error": { … } }.
Without --json, an error prints BlueBot’s message, hint, documentation link and request id:
Error: Send `to` or `chat_id`. (invalid_request)
Hint: Check the fields named in the message and send it again.
Docs: https://usebluebot.com/docs/errors/invalid_request
Request: req_1Hq2LxW9cVbN3mKpRtYs0aColour, BlueBot’s face and other decoration never appear in errors or in --json output.
Exit codes
| Code | Meaning |
|---|---|
0 | It worked. listen and --follow exit 0 when you press Ctrl-C. |
1 | BlueBot refused the request (any error code but those below), or something unexpected happened. |
2 | The command was wrong: an unknown command or flag, or a missing argument. |
3 | Not signed in, or the key was refused: unauthorized or insufficient_scope. |
4 | BlueBot couldn’t be reached, after retries. |
5 | send --wait: the text failed, expired, was canceled or is unconfirmed. |
The CLI retries what is safe to retry, as the SDKs do: connection failures, 429 and 5xx answers, twice, waiting what BlueBot asks.
Settings and environment
| Variable | What it does |
|---|---|
BLUEBOT_API_KEY | Use this key instead of the stored one. --api-key beats it. |
BLUEBOT_PROFILE | The profile to use when --profile isn’t given. |
BLUEBOT_BASE_URL | The API to talk to. Default https://usebluebot.com/api/v1. |
BLUEBOT_CONFIG_DIR | Where the CLI keeps its settings. |
NO_COLOR | Any value turns colour off. |
BLUEBOT_LOG | debug prints each request’s method, path, status, time and request id to standard error. Bodies are never printed. |
Settings live in config.json in the config folder: $BLUEBOT_CONFIG_DIR, else $XDG_CONFIG_HOME/bluebot, else ~/.config/bluebot on macOS and Linux and %APPDATA%\bluebot on Windows. It holds each profile’s account, the start of its key and its base URL; keys and local signing secrets live in the keychain, or in credentials.json (mode 0600) where there is none.