# Send emails with the CLI

> Install the postshiba command, sign in, and send from your terminal.

## What you'll build

A receipt email sent from your terminal with `postshiba`, then a few account commands you can reuse in scripts.

## Before you start

- An API key with **Full access**. See [Create an API key](/docs/create-an-api-key). Support keys cannot send.
- A verified sending domain. See [Add and verify a domain](/docs/add-a-domain). `from` must be an address on it.
- A cluster that is ready to send (`sending_ready` is `true`). See [Clusters](/docs/dashboard/clusters/introduction).
- Your team id. It is on the **API keys** page.
- Node.js 20 or later.

## Install

```sh install.sh
npm install -g @postshiba/cli
```

For a one-off run without installing:

```sh npx.sh
npx @postshiba/cli
```

On a terminal, `postshiba` with no arguments opens a menu: send an email, add a sending domain, inspect clusters and inboxes, check your setup, or install the agent skill. Missing fields become prompts, with a spinner and a table for the result.

## Sign in

Run `postshiba login`. It asks for the API key, checks it, then prints `Signed in as` your email. It then asks for your team id, and offers a default cluster if you have one. The key is never printed.

```sh login.sh
postshiba login
```

Or set environment variables and skip the config file:

```sh env.sh
export POSTSHIBA_API_KEY=YOUR_API_KEY
export POSTSHIBA_TEAM_ID=KjkAJW
export POSTSHIBA_CLUSTER_ID=NmQpXr
```

Each invocation reads a flag first, then the matching env var, then `~/.config/postshiba/config.json`. A missing key is a usage error that names `postshiba login` and `POSTSHIBA_API_KEY`. A team-scoped command without a team id names `--team`. `postshiba whoami` reprints the signed-in user. `postshiba logout` deletes the config file.

## Send your first email

On a terminal, `postshiba send` prompts for anything missing. `from` suggests `hello@` on a verified domain. `to` is comma-separated. Then pick a body: plain text, an HTML file, or a published template. Confirm the preview, and the success line includes the message id.

To skip the prompts, pass the flags:

```sh send.sh
postshiba send \
  --from hello@mail.example.com \
  --to you@example.com \
  --subject "Your receipt" \
  --text "Thanks for your order." \
  --arg order_id=ord_123
```

The message then shows under Events in the dashboard. `from` and at least one `--to` are required. Repeat `--to` for more recipients. Pass `--cluster NmQpXr` to pin a cluster. Without it, the CLI uses the default from login or `POSTSHIBA_CLUSTER_ID`, or lets the API pick a sending-ready cluster.

With `--json`, success prints the API body:

```json response.json
{
  "queued": true,
  "message_id": "abc@capsule.test",
  "from": "hello@mail.example.com",
  "to": ["you@example.com"],
  "subject": "Your receipt",
  "unique_args": {"order_id": "ord_123"}
}
```

## Templates and sandbox tests

Pass `--template` with the alias or public id and `--var key=value` for each Liquid variable. Leave out `--text` and `--html`.

```sh template.sh
postshiba send \
  --from hello@mail.example.com \
  --to you@example.com \
  --template welcome \
  --var name=Ada \
  --cluster NmQpXr
```

`--sandbox` runs every send check without delivering. It requires `--cluster` and posts to the cluster send path.

```sh sandbox.sh
postshiba send \
  --from hello@mail.example.com \
  --to you@example.com \
  --subject "Your receipt" \
  --text "Thanks for your order." \
  --cluster NmQpXr \
  --sandbox
```

`--idempotency-key` also uses that cluster path. See [Sending](/docs/api-reference/sending).

## Manage your account from the terminal

Commands follow `postshiba <resource> <action>`. Positional ids follow the path. Rows that create or update take `--data` as JSON, `@file.json`, or `-` for stdin.

| Resource | Example |
| --- | --- |
| `clusters` | `postshiba clusters get NmQpXr` |
| `sending-domains` | `postshiba sending-domains create --data '{"name":"mail.example.com"}'` |
| `messages` | `postshiba messages get PqRzMn GxTyVu` |
| `events` | `postshiba events list NmQpXr` |
| `smtp-credentials` | `postshiba smtp-credentials create NmQpXr --data @cred.json` |
| `firewall` | `postshiba firewall add-entry --data @entry.json` |

Also `network`, `tenants`, `inboxes`, `webhooks`, `templates`, and `suppressions`. `postshiba sending-domains make-primary HsVtYk` promotes a domain. `postshiba messages download-attachment PqRzMn GxTyVu 1 --output photo.png` writes the file. `postshiba help clusters` lists the actions for a resource.

```sh doctor.sh
postshiba doctor
```

`doctor` checks that the key is valid, a team id is set, a cluster is `sending_ready`, and a sending domain is verified. Each failure prints the command that fixes it.

## Scripts and agents

Interactive mode needs a TTY, no `--json`, and `CI` unset. Otherwise the CLI is plain: no prompts, and a successful call prints the API JSON with 2-space indent. `--no-input` forces that. A missing flag is then a usage error.

| Flag | Effect |
| --- | --- |
| `--json` | Print API JSON. Never prompt. |
| `--no-input` | Never prompt. Missing input exits 2. |
| `--yes` | Skip the confirm on `delete`, `suspend`, `release`, and `unassign`. Plain mode requires it for those actions. |

| Exit | Meaning |
| --- | --- |
| `0` | Success. |
| `1` | The API returned an error. |
| `2` | Usage error: unknown command, missing id, missing `--data`, or bad JSON. The message points at `postshiba help <resource>`. |

The CLI ships an agent skill that teaches an assistant to pass `--json`, read credentials from the environment, run `doctor --json` first, and use `--sandbox` for tests. `--yes` only when you asked for a destructive action.

```sh skills.sh
postshiba skills install
```

That writes `.cursor/skills/postshiba-cli/` in this project. Pass `--global` to install under your home directory, or `--target claude` or `--target agents` for those layouts. You can also run `npx skills add postshiba/postshiba-cli`. The same skill is in the PostShiba Cursor plugin.

## Troubleshooting

A non-2xx response prints `Error: <message>` on stderr and, when present, `(field: <field>)`. Then the process exits 1.

| Status | `error` | What the CLI does |
| --- | --- | --- |
| `401` | `Invalid token or no user signed in` | Prints the error. The API key is missing, mistyped, or deleted. Copy it again from **API keys**, or run `postshiba login`. |
| `403` | `forbidden`, `domain_unverified`, or `kyc_required` | Prints the error. Use a Full access key and a verified `from`. While your team is pending production approval, send only to team members, live inbox addresses, and inbox forward-to addresses. |
| `422` | `invalid` | Prints the error and `field` when the API names one, such as a missing `to`, an unpublished template, or a rejected attachment. |
| `429` | `throttled` | Prints the error, then `The cluster hit its hourly send limit. Wait until the next hour before retrying.` Do not retry in that window. See [What happens at your cap](/docs/dashboard/clusters/what-happens-at-your-cap). |

Every other status is on [Errors](/docs/api-reference/errors).

## Next steps

- [Choose a send path](/docs/sending-emails) if you need SMTP or HTTP inject for volume.
- [Unique args](/docs/dashboard/emails/unique-args) to get your own ids back on delivery webhooks.
- [Connect MCP](/docs/connect-mcp) to query PostShiba from Cursor.
- [SDKs](/docs/sdks) for every language client.
