# 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.

```bash
npm install --global @usebluebot/cli
bluebot --version
```

To 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

```bash
bluebot login
```

`login` 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.

```bash
bluebot whoami
```

```text
Signed 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   default
```

## Global 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

```bash
bluebot send +15555550123 "Your table for four is ready."
```

```text
→ +1 555-555-0123  queued  msg_1Hq2LxW9cVbN3mKpRtYs0a
```

`bluebot 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:

```text
→ +1 555-555-0123  queued, held: quiet hours until 8:00 AM  msg_1Hq2LxW9cVbN3mKpRtYs0a
```

Texts from the CLI show `source: "cli"` in BlueBot.

## Read your texts

```bash
bluebot messages
bluebot messages --direction in --since 24h
bluebot messages --follow
```

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

```bash
bluebot status
```

```text
Mac       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 normally
```

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

```bash
bluebot lookup +15555550123
```

```text
+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.

```bash
bluebot listen --forward-to http://localhost:3000/bluebot/webhooks
```

```text
Ready. 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 asks `GET /v1/events` for 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 as `BLUEBOT_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 `--since` to 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`. [Testing webhooks](https://usebluebot.com/docs/webhooks/testing) 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`:

```bash
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-json` and `messages --follow --json` print 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:

```text
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_1Hq2LxW9cVbN3mKpRtYs0a
```

Colour, 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.
