---
title: "Contacts, audiences and broadcasts"
description: "Every command for the address book, audiences, broadcasts and the suppression list, with worked examples."
url: "https://nextmail.sh/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 `nextmail 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`.

> `nextmail <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 |
| --- | --- |
| nextmail contacts list | One page of the saved contacts, most recently mailed first. `--source` keeps `manual` or `auto` contacts, and `--q` searches names and addresses |
| nextmail contacts get <email> | One contact, with every audience it is in |
| nextmail contacts create --email <value> | Save a new contact, with `--name`, `--notes`, `--audience-ids` and custom fields in `--attributes`. An address already in the book is refused with 409 `contact_exists` |
| nextmail contacts update <email> | Change `--name` or `--notes`, where `null` clears one, or custom fields with `--attributes`, a JSON object where `null` removes a field. The address itself cannot change |
| nextmail contacts delete <email> | Delete the contact with its notes, photo and memberships, and hide the address so the composer does not record it again |
| nextmail contacts set-audiences <email> --audience-ids <a,b> | Make the audiences the contact is in exactly this list. The default audience is always kept |
| nextmail 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 |
| nextmail 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 |
| nextmail contacts delete-many <emails...> | Delete and hide 1 to 200 addresses in one call |
| nextmail contacts set-photo <email> <data> | Upload the photo from a file, or from stdin with `-`: PNG, JPEG, WebP or GIF up to 5 MB |
| nextmail contacts remove-photo <email> | Take the photo off and delete the stored image |
| nextmail contacts block <email> | Put the address on the workspace blocklist, so mail from it is refused. A plus tag is dropped |
| nextmail contacts unblock <email> | Take off every blocklist rule that blocks the address, a whole-domain rule included |
| nextmail contacts list-threads <email> | The threads the address wrote or was written to, in every folder. `--q` searches inside them |
| nextmail 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 |
| nextmail contacts import-vcf <data> | Import a .vcf file, read from a path or from stdin with `-`. Every card becomes a contact card, and imported contacts join no audience |
| nextmail contacts list-imports | List one page of the contacts imports, newest first, with what each added and left out |
| nextmail contacts get-import <id> | Read one contacts import with its report |
| nextmail contacts undo-import <id> | Remove every card one import added. Asks you to confirm |

## 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 |
| --- | --- |
| nextmail audiences list | One page of the audiences, the default one first and the rest newest first, each with its `contactCount` |
| nextmail audiences growth | How the audiences grew over a window, 30 days unless `--days` or `--minutes` says otherwise: joins and unsubscribes per bucket, and totals |
| nextmail audiences get <id> | One audience, with a fresh `contactCount` |
| nextmail audiences create --name <value> | Create an empty audience, with an optional `--description`. Names are not unique |
| nextmail audiences update <id> | Change `--name` or `--description`. Membership is not touched |
| nextmail audiences delete <id> | Delete the audience and keep its contacts. The default audience cannot be deleted |
| nextmail audiences empty <id> | Take every contact out and keep the audience, with its id, name and description |
| nextmail 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 |
| nextmail audiences add-contact <id> --email <value> | Put one existing contact in the audience. Adding somebody who is already there changes nothing |
| nextmail audiences remove-contact <id> <email> | Take one contact out. A contact that is not in the audience is a 404 |
| nextmail audiences add-contacts <id> --emails <a,b> | Put up to 200 existing contacts in, and report the addresses that are not contacts in `missing` |
| nextmail audiences remove-contacts <id> --emails <a,b> | Take up to 200 contacts out, and report the ones that were not in it |
| nextmail 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 |
| --- | --- |
| nextmail 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 |
| nextmail broadcasts send --audience-ids <a,b> --from <value> | Send with `--subject` and `--html` or `--text`, or a stored `--template`, now or at `--scheduled-at` |
| nextmail broadcasts list | One page of broadcasts, newest first, with live counts. `--audience-id` keeps the ones sent to that audience |
| nextmail broadcasts get <id> | One broadcast, with its status and live counts: the command to poll while it sends |
| nextmail broadcasts stats <id> | Totals of delivered, bounced, opened, clicked and unsubscribed, and a series per `--grain` bucket, an hour unless you say. `--days` or `--minutes` adds a `window` of what happened lately |
| nextmail broadcasts analytics | How the broadcasts sent over a window did, 30 days unless `--days` or `--minutes` says otherwise: totals, a series per `--grain` bucket and a row per broadcast. `--broadcast-ids` picks the ones added up |
| nextmail broadcasts list-recipients <id> | Who each copy went to and what happened to it. `--filter` keeps one group, such as `bounced` or `not_opened` |
| nextmail broadcasts get-recipient <id> <email-id> | One person's copy, with the subject, HTML and text exactly as they received them |
| nextmail 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 |
| --- | --- |
| nextmail suppressions list | One page of the list, newest first. `--reason` keeps `bounce`, `complaint` or `manual`, and `--q` searches |
| nextmail suppressions get <id> | One row: the address, the reason, the detail the bounce or complaint carried, and whether it can be removed |
| nextmail suppressions add --email <value> | Stop sending to an address. Adding one that is there already returns the row it holds |
| nextmail 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`, `analytics`, `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=$(nextmail audiences create --name 'Product updates' --json | jq -r .id)
nextmail audiences import-contacts "$AUDIENCE" --contacts @contacts.json
nextmail 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**

```
nextmail broadcasts send --data @broadcast.json --dry-run
BROADCAST=$(nextmail broadcasts send --data @broadcast.json --yes --json | jq -r .id)
nextmail broadcasts get "$BROADCAST"
nextmail 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**

```
nextmail broadcasts list-recipients brd_5a8c1e3f7b2d94a06c8e1f3b --filter bounced --ndjson | jq -r .email
nextmail broadcasts list-recipients brd_5a8c1e3f7b2d94a06c8e1f3b --filter not_opened --all --json | jq ".items | length"
nextmail 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**

```
nextmail audiences list-contacts aud_9f2c4b7e1a0d63d84c5f2e7b --statuses subscribed --max 200 --ndjson \
  | jq -s '{ emails: map(.email) }' \
  | nextmail 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**

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

```
nextmail suppressions add --email noreply@example.com
nextmail suppressions list --q ada@example.com --json | jq -r '.items[] | select(.removable) | .id'
nextmail suppressions remove 7b1e2c3d-4f5a-4b6c-8d7e-9f0a1b2c3d4e --yes
nextmail 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 `nextmail 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://nextmail.sh/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 |
| --- | --- |
| nextmail contacts list | 1 to 200, 50 unless `--limit` says otherwise |
| nextmail contacts list-people | 1 to 100, 25 unless `--limit` says otherwise |
| nextmail contacts list-threads | 1 to 100, 25 unless `--limit` says otherwise |
| nextmail audiences list | 1 to 100, 25 unless `--limit` says otherwise |
| nextmail audiences list-contacts | 1 to 200, 50 unless `--limit` says otherwise |
| nextmail broadcasts list | 1 to 100, 25 unless `--limit` says otherwise |
| nextmail broadcasts list-recipients | 1 to 200, 50 unless `--limit` says otherwise |
| nextmail 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 custom field of the contact is `{{attributes.<key>}}` in `--subject`, `--html` and `--text`, such as `{{attributes.plan|Free}}`.
- 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://nextmail.sh/docs/sdk/contacts.md): The same calls as SDK methods, with every field they return.
- [Audiences in the SDK](https://nextmail.sh/docs/sdk/audiences.md): Membership, imports and growth, method by method.
- [Broadcasts in the SDK](https://nextmail.sh/docs/sdk/broadcasts.md): Merge fields, unsubscribe and the counts, in depth.
- [Suppressions in the SDK](https://nextmail.sh/docs/sdk/suppressions.md): The list the workspace will not send to.
