---
title: "Contacts, audiences and broadcasts"
description: "Every command for the address book, audiences, broadcasts and the suppression list, with worked examples."
url: "https://openemail.uk/docs/cli/contacts"
area: "CLI"
category: "Commands by area"
---

# Contacts, audiences and broadcasts

Every command for the address book, audiences, broadcasts and the suppression list, with worked examples.

## How they fit together

Four namespaces cover the people you write to. Contacts are the workspace address book, audiences are named lists of contacts, a broadcast sends one message to everybody in some audiences, and the suppression list holds the addresses the workspace will not send to. Each command calls one SDK method, so the SDK pages describe the same calls in more depth.

- A contact has no id. Its address is the key every `contacts` command takes, trimmed and lower cased, so `Grace@Example.com` and `grace@example.com` are one contact. An audience has an `aud_` id, a broadcast a `brd_` id, and a suppression the id `suppressions list` prints.
- Every contact is in the default audience for as long as it exists. That audience cannot be deleted, emptied or thinned, and `builtin` is `default` on it.
- The address book belongs to the workspace, so every member and every key reads and writes the same one.
- Each namespace also answers to its singular, as in `openemail contact get`, and the usual aliases work: `ls`, `show`, `new`, `edit` and `rm`. In `suppressions`, whose verbs are `add` and `remove`, `new` leads to `add` and `rm` to `remove`.

> `openemail <namespace> <verb> --help` shows every flag with its type, the scopes, the endpoint and what the command returns. Add `--json` for the same page as data.

## Contacts

The workspace address book: the people a member has written to from the app composer, plus anybody saved by hand. Mail that arrives adds nobody, and neither does a send through the API or the CLI.

| Command | What it does |
| --- | --- |
| openemail contacts list | One page of the saved contacts, most recently mailed first. `--source` keeps `manual` or `auto` contacts, and `--q` searches names and addresses |
| openemail contacts get <email> | One contact, with every audience it is in |
| openemail contacts create --email <value> | Save a new contact, with `--name`, `--notes` and `--audience-ids`. An address already in the book is refused with 409 `contact_exists` |
| openemail contacts update <email> | Change `--name` or `--notes`, where `null` clears one. The address itself cannot change |
| openemail contacts delete <email> | Delete the contact with its notes, photo and memberships, and hide the address so the composer does not record it again |
| openemail contacts set-audiences <email> --audience-ids <a,b> | Make the audiences the contact is in exactly this list. The default audience is always kept |
| openemail contacts list-people | Everyone on the Contacts page: the saved contacts and, with `threads:read`, every address seen in mail, with thread counts. `--sort`, `--q`, `--email` and `--blocked` narrow it |
| openemail contacts save <email> | Save an address, keep one recorded from a send, or bring back a deleted one. Never an error, whatever state the address is in |
| openemail contacts delete-many <emails...> | Delete and hide 1 to 200 addresses in one call |
| openemail contacts set-photo <email> <data> | Upload the photo from a file, or from stdin with `-`: PNG, JPEG, WebP or GIF up to 5 MB |
| openemail contacts remove-photo <email> | Take the photo off and delete the stored image |
| openemail contacts block <email> | Put the address on the workspace blocklist, so mail from it is refused. A plus tag is dropped |
| openemail contacts unblock <email> | Take off every blocklist rule that blocks the address, a whole-domain rule included |
| openemail contacts list-threads <email> | The threads the address wrote or was written to, in every folder. `--q` searches inside them |
| openemail contacts activity <email> | Mail received from and sent to the address over a window, 90 days unless `--minutes` says otherwise, with the threads waiting on a reply and the median reply time each way |

## Audiences

Named lists of contacts, up to 100 in a workspace. An address has to be a contact before it joins one, except through `import-contacts`, which saves new addresses as it goes.

| Command | What it does |
| --- | --- |
| openemail audiences list | One page of the audiences, the default one first and the rest newest first, each with its `contactCount` |
| openemail audiences growth | How the audiences grew over a window, 30 days unless `--days` or `--minutes` says otherwise: joins and unsubscribes per bucket, and totals |
| openemail audiences get <id> | One audience, with a fresh `contactCount` |
| openemail audiences create --name <value> | Create an empty audience, with an optional `--description`. Names are not unique |
| openemail audiences update <id> | Change `--name` or `--description`. Membership is not touched |
| openemail audiences delete <id> | Delete the audience and keep its contacts. The default audience cannot be deleted |
| openemail audiences empty <id> | Take every contact out and keep the audience, with its id, name and description |
| openemail audiences list-contacts <id> | One page of the contacts in the audience, with when each joined and whether it unsubscribed. `--sort`, `--q`, `--source` and `--statuses` narrow it |
| openemail audiences add-contact <id> --email <value> | Put one existing contact in the audience. Adding somebody who is already there changes nothing |
| openemail audiences remove-contact <id> <email> | Take one contact out. A contact that is not in the audience is a 404 |
| openemail audiences add-contacts <id> --emails <a,b> | Put up to 200 existing contacts in, and report the addresses that are not contacts in `missing` |
| openemail audiences remove-contacts <id> --emails <a,b> | Take up to 200 contacts out, and report the ones that were not in it |
| openemail audiences import-contacts <id> --contacts <json\|@file\|-> | Import up to 500 `{ email, name }` rows, saving the addresses that are not contacts yet |

## Broadcasts

One message to everybody in up to 10 audiences, sent as a separate copy for each person, with the merge fields filled in and an unsubscribe link. Each copy is an ordinary email with its own `msg_` id, events and webhooks.

| Command | What it does |
| --- | --- |
| openemail broadcasts preview --audience-ids <a,b> | Count who a broadcast to these audiences would reach, and who it would skip as unsubscribed or suppressed. Sends nothing |
| openemail broadcasts send --audience-ids <a,b> --from <value> | Send with `--subject` and `--html` or `--text`, or a stored `--template`, now or at `--scheduled-at` |
| openemail broadcasts list | One page of broadcasts, newest first, with live counts. `--audience-id` keeps the ones sent to that audience |
| openemail broadcasts get <id> | One broadcast, with its status and live counts: the command to poll while it sends |
| openemail broadcasts stats <id> | Totals of delivered, bounced, opened, clicked and unsubscribed, and a series per `--grain` bucket, an hour unless you say |
| openemail broadcasts list-recipients <id> | Who each copy went to and what happened to it. `--filter` keeps one group, such as `bounced` or `not_opened` |
| openemail broadcasts get-recipient <id> <email-id> | One person's copy, with the subject, HTML and text exactly as they received them |
| openemail broadcasts cancel <id> | Stop a broadcast that is scheduled, queued or still sending. Copies that have gone cannot be recalled |

## Suppressions

The addresses this workspace will not send to: hard bounces and complaints, recorded as they happen, and any address you add by hand. A send to one is refused for that recipient before anything leaves.

| Command | What it does |
| --- | --- |
| openemail suppressions list | One page of the list, newest first. `--reason` keeps `bounce`, `complaint` or `manual`, and `--q` searches |
| openemail suppressions get <id> | One row: the address, the reason, the detail the bounce or complaint carried, and whether it can be removed |
| openemail suppressions add --email <value> | Stop sending to an address. Adding one that is there already returns the row it holds |
| openemail suppressions remove <id> | Allow mail to the address again. A hard bounce cannot be removed |

> Suppressions and the blocklist are different lists. `suppressions add` stops mail going out to an address, and `contacts block` refuses mail coming in from it.

## Scopes

Most commands need the read or write scope of their namespace. A few need another, because they read or change something else:

| Scope | Commands |
| --- | --- |
| contacts:read | `contacts list`, `get` and `list-people` |
| contacts:write | `contacts create`, `update`, `delete`, `save`, `delete-many`, `set-photo` and `remove-photo`, and `audiences import-contacts` beside `audiences:write` |
| audiences:read | `audiences list`, `growth`, `get` and `list-contacts`, and `broadcasts preview`, so a key that cannot send can still show the count |
| audiences:write | Every other `audiences` command, and `contacts set-audiences`. `contacts create --audience-ids` needs it beside `contacts:write` |
| threads:read | `contacts list-threads` and `activity`, and the addresses seen in mail in `list-people` |
| settings:read | `suppressions list` and `get` |
| settings:write | `suppressions add` and `remove`, and `contacts block` and `unblock` |
| emails:read | `broadcasts list`, `get`, `stats`, `list-recipients` and `get-recipient` |
| emails:send | `broadcasts send`, which needs `audiences:read` too, and `broadcasts cancel` |

- A key limited to particular addresses or domains reads and writes the same address book as every other key. It sees only the broadcasts sent from an address or domain it holds, gets the saved contacts alone from `list-people`, and is refused with 422 `capability_unsupported` by `contacts list-threads`, `activity`, `block` and `unblock`, and by `suppressions add` and `remove`.
- A browser sign-in by a member who reaches only some addresses is refused with 422 `capability_unsupported` on every `contacts`, `audiences` and `broadcasts` command. `suppressions add` refuses a browser sign-in by anybody but the workspace owner.

## Worked examples

Build an audience from a file, then count who a broadcast to it would reach. `import-contacts` saves the addresses that are not contacts yet, and running it again creates and adds nothing twice.

**contacts.json**

```
[
  { "email": "ada@example.com", "name": "Ada Lovelace" },
  { "email": "grace@example.com", "name": "Grace Hopper" },
  { "email": "katherine@example.com" }
]
```

**Build the audience and count it**

```
AUDIENCE=$(openemail audiences create --name 'Product updates' --json | jq -r .id)
openemail audiences import-contacts "$AUDIENCE" --contacts @contacts.json
openemail broadcasts preview --audience-ids "$AUDIENCE"
```

Check a broadcast with `--dry-run`, which prints the request and sends nothing, then send it. The broadcast is created at once and sent in the background, so poll `get` to follow it. This body does not place `{{unsubscribeUrl}}`, so every copy gets a one-line unsubscribe footer.

**broadcast.json**

```
{
  "audienceIds": ["aud_4c1b8e2a7d9f05c36b4e8a71"],
  "from": "Acme <news@acme.com>",
  "subject": "{{firstName|Hello}}, the September release is out",
  "html": "<p>Hi {{firstName|there}},</p><p>Here is what changed this month.</p>",
  "scheduledAt": "2026-10-01T09:00:00Z"
}
```

**Check the broadcast, then send it**

```
openemail broadcasts send --data @broadcast.json --dry-run
BROADCAST=$(openemail broadcasts send --data @broadcast.json --yes --json | jq -r .id)
openemail broadcasts get "$BROADCAST"
openemail broadcasts stats "$BROADCAST" --grain day
```

See who a broadcast did not reach. `--ndjson` prints one recipient per line, and `--all --json` one document with every page.

**Who it did not reach**

```
openemail broadcasts list-recipients brd_5a8c1e3f7b2d94a06c8e1f3b --filter bounced --ndjson | jq -r .email
openemail broadcasts list-recipients brd_5a8c1e3f7b2d94a06c8e1f3b --filter not_opened --all --json | jq ".items | length"
openemail suppressions list --reason bounce --all --max 50
```

Copy the subscribed members of one audience into another. `jq` turns the stream into the body `add-contacts` takes, and `--data -` reads it from stdin. `--max 200` keeps it to the 200 addresses one call accepts.

**Copy subscribed members**

```
openemail audiences list-contacts aud_9f2c4b7e1a0d63d84c5f2e7b --statuses subscribed --max 200 --ndjson \
  | jq -s '{ emails: map(.email) }' \
  | openemail audiences add-contacts aud_1c4e7a9b2d0f36e85a7c1b4d --data -
```

Delete every contact the composer recorded at one domain. `delete-many` takes up to 200 addresses a call, so `xargs -n 200` splits a longer list. Check the batches with `--dry-run` first, because there is no undo.

**Delete by domain**

```
openemail contacts list --source auto --all --ndjson \
  | jq -r 'select(.email | endswith("@old-vendor.example")) | .email' > leaving.txt
xargs -n 200 openemail contacts delete-many --dry-run < leaving.txt
xargs -n 200 openemail contacts delete-many --yes < leaving.txt
```

Stop sending to an address, allow one again, and block a sender. `removable` says which rows `suppressions remove` will take.

**Suppress, allow and block**

```
openemail suppressions add --email noreply@example.com
openemail suppressions list --q ada@example.com --json | jq -r '.items[] | select(.removable) | .id'
openemail suppressions remove 7b1e2c3d-4f5a-4b6c-8d7e-9f0a1b2c3d4e --yes
openemail contacts block spammer@example.com
```

## Confirmations and verification codes

These commands ask you to confirm in a terminal before they run:

| Namespace | Asks to confirm |
| --- | --- |
| contacts | `delete`, `delete-many`, `remove-photo` and `unblock` |
| audiences | `delete`, `empty`, `remove-contact` and `remove-contacts` |
| broadcasts | `send` and `cancel` |
| suppressions | `remove` |

- `--yes` confirms for you. Unattended, with `--json` or `--no-input`, in CI or without a terminal, a command that would ask stops with `Refusing to run unattended. Pass --yes to confirm.` and exit code `2`.
- `--dry-run` prints the request the command would send and exits with code `0`, without asking and without changing anything.
- With a browser sign-in, `audiences delete` asks for a verification code first, as the web app does. `--yes` never skips it, and unattended the command stops with exit code `4`. Run `openemail verify` beforehand, or use an API key, which is never asked.
- `audiences empty` never asks for a verification code, so check the id before you pass `--yes`.

- [Verification codes](https://openemail.uk/docs/cli/authentication.md): How the CLI asks for a code, and how long one lasts.

## Paging

Every command that lists reads one page. When more remain, pass the cursor it printed to `--cursor`, with the same filters, or read them all:

- `--all` reads every page and streams the items: a table on a terminal, and one JSON object per line when piped or with `--ndjson`.
- `--max <n>` stops after that many items, and implies `--all`.
- `--json` prints one `{ items, hasMore, nextCursor }` document, `--all` included.
- A malformed or stale cursor is a 400 `invalid_cursor`. Start again without one.

| Command | Page size |
| --- | --- |
| openemail contacts list | 1 to 200, 50 unless `--limit` says otherwise |
| openemail contacts list-people | 1 to 100, 25 unless `--limit` says otherwise |
| openemail contacts list-threads | 1 to 100, 25 unless `--limit` says otherwise |
| openemail audiences list | 1 to 100, 25 unless `--limit` says otherwise |
| openemail audiences list-contacts | 1 to 200, 50 unless `--limit` says otherwise |
| openemail broadcasts list | 1 to 100, 25 unless `--limit` says otherwise |
| openemail broadcasts list-recipients | 1 to 200, 50 unless `--limit` says otherwise |
| openemail suppressions list | 1 to 100, 25 unless `--limit` says otherwise |

## Good to know

- `contacts create` refuses an address already in the book with 409 `contact_exists`, so a retry never overwrites a name somebody edited. `contacts save` never refuses: it saves, keeps or brings back the address, whatever state it is in.
- `contacts delete` also takes an address that was only ever seen in mail, which takes that person off `list-people`. The mail stays. There is no undo: saving the address again starts a contact with no name, no notes and no audience beyond the default one.
- The address is the identity of a contact, so `contacts update` cannot change it. Moving a contact is a `delete` and a `create`.
- `contacts set-photo` reads the image from a file, or from stdin with `-`. Pass `--content-type`, such as `image/jpeg`: without it the image can go as `application/octet-stream`, which the server refuses with 422 `invalid_image`.
- `broadcasts send --scheduled-at` takes an ISO 8601 time such as `2026-10-01T09:00:00Z`, or an ISO 8601 duration such as `PT2H` or `P1D`, up to 365 days out. The short delays `send --at` takes, such as `2h`, are refused here.
- Merge fields work in `--subject`, `--html` and `--text`: `{{firstName}}`, `{{lastName}}`, `{{name}}`, `{{email}}` and `{{unsubscribeUrl}}`, each with a fallback after a bar, as in `{{firstName|there}}`. A body that does not place `{{unsubscribeUrl}}` gets a one-line unsubscribe footer. A template is sent as it is, so put the link in the template.
- A broadcast is checked against the monthly sends of the plan before anything is written, and each copy counts as one send. One the allowance cannot cover is refused with 429 `send_quota_exceeded`, and nothing is left behind.
- Pass your own `--idempotency-key` to `broadcasts send` when a script may run the step again. The same key answers with the broadcast it created instead of sending a new one.
- A contact who unsubscribes from a broadcast stays in the audience with `unsubscribedAt` set, and later broadcasts to that audience skip it. `audiences list-contacts --statuses unsubscribed` lists them.
- A hard bounce stays on the suppression list. `suppressions remove` refuses it with 409 `suppression_not_removable`, and `removable` on each row says so in advance.

## Related

- [Contacts in the SDK](https://openemail.uk/docs/sdk/contacts.md): The same calls as SDK methods, with every field they return.
- [Audiences in the SDK](https://openemail.uk/docs/sdk/audiences.md): Membership, imports and growth, method by method.
- [Broadcasts in the SDK](https://openemail.uk/docs/sdk/broadcasts.md): Merge fields, unsubscribe and the counts, in depth.
- [Suppressions in the SDK](https://openemail.uk/docs/sdk/suppressions.md): The list the workspace will not send to.
