Skip to content

Commit b6d394c

Browse files
feat: add suppression endpoints support
1 parent a5536b0 commit b6d394c

21 files changed

Lines changed: 1379 additions & 12 deletions

File tree

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "resend-cli",
3-
"version": "2.8.1",
3+
"version": "2.9.0",
44
"description": "The official CLI for Resend",
55
"license": "MIT",
66
"repository": {
@@ -46,7 +46,7 @@
4646
"esbuild": "0.28.1",
4747
"esbuild-wasm": "0.28.0",
4848
"picocolors": "1.1.1",
49-
"resend": "6.17.1"
49+
"resend": "6.18.0-canary.0"
5050
},
5151
"pkg": {
5252
"scripts": [

pnpm-lock.yaml

Lines changed: 9 additions & 9 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

skills/resend-cli/SKILL.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ metadata:
1313
author: resend
1414
# Skill version is independent from the CLI/package.json version —
1515
# bump it on skill content changes, not CLI releases.
16-
version: "2.3.1"
16+
version: "2.4.0"
1717
homepage: https://resend.com/docs/cli-agents
1818
source: https://github.com/resend/resend-cli
1919
openclaw:
@@ -57,6 +57,7 @@ references:
5757
- references/templates.md
5858
- references/topics.md
5959
- references/logs.md
60+
- references/suppressions.md
6061
- references/webhooks.md
6162
- references/auth.md
6263
- references/workflows.md
@@ -143,6 +144,7 @@ Auth resolves: `--api-key` flag > `RESEND_API_KEY` env > config file (`resend lo
143144
| `emails receiving` | list, get, attachments, forward, listen |
144145
| `domains` | create, verify, get, claim, update, delete, list |
145146
| `logs` | list, get, open |
147+
| `suppressions` _(beta)_ | list, add, get, delete, batch — requires account enrollment |
146148
| `api-keys` | create, list, delete |
147149
| `automations` | create, get, list, update, delete, stop, open, runs |
148150
| `events` | create, get, list, update, delete, send, open |
@@ -220,6 +222,7 @@ resend doctor -q
220222
- **Defining contact properties**[references/contact-properties.md](references/contact-properties.md)
221223
- **Working with templates**[references/templates.md](references/templates.md)
222224
- **Viewing API request logs**[references/logs.md](references/logs.md)
225+
- **Managing the suppression list** (beta) → [references/suppressions.md](references/suppressions.md)
223226
- **Creating automations or sending events**[references/automations.md](references/automations.md)
224227
- **Setting up webhooks or listening for events**[references/webhooks.md](references/webhooks.md)
225228
- **Auth, profiles, or health checks**[references/auth.md](references/auth.md)
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
# suppressions
2+
3+
Detailed flag specifications for `resend suppressions` commands.
4+
5+
> **Beta:** Suppressions is a pre-GA feature gated per account. Commands appear in
6+
> `--help` but return an API error unless the suppression list is enabled for your
7+
> account. Reach out to Resend to join the beta.
8+
9+
Suppressions block future sends to an address. Each entry has an `origin`:
10+
11+
| Origin | Meaning |
12+
|--------|---------|
13+
| `bounce` | Added automatically after a hard bounce |
14+
| `complaint` | Added automatically after a spam complaint |
15+
| `manual` | Added by you via `suppressions add` |
16+
17+
`get` and `delete` accept **either** a suppression ID **or** the email address.
18+
19+
---
20+
21+
## suppressions list
22+
23+
List suppressed addresses (default subcommand — `resend suppressions` alone runs it).
24+
25+
| Flag | Type | Required | Description |
26+
|------|------|----------|-------------|
27+
| `--limit <n>` | number | No | Max results, 1-100 (default 10) |
28+
| `--after <cursor>` | string | No | Forward pagination cursor |
29+
| `--before <cursor>` | string | No | Backward pagination cursor |
30+
| `--origin <origin>` | string | No | Filter: `bounce` \| `complaint` \| `manual` |
31+
32+
**Alias:** `ls`
33+
34+
**Output:** `{"object":"list","has_more":false,"data":[{"object":"suppression","id":"...","email":"...","origin":"bounce|complaint|manual","source_id":"..."|null,"created_at":"..."}]}`
35+
36+
---
37+
38+
## suppressions add
39+
40+
Suppress a single email address (origin `manual`).
41+
42+
**Argument:** `<email>` — email address to suppress (required in non-interactive mode)
43+
44+
**Output:** `{"object":"suppression","id":"..."}`
45+
46+
---
47+
48+
## suppressions get
49+
50+
Retrieve a single suppression.
51+
52+
**Argument:** `<id-or-email>` — suppression ID or the suppressed email address
53+
54+
**Output:** `{"object":"suppression","id":"...","email":"...","origin":"...","source_id":"..."|null,"created_at":"..."}`
55+
56+
---
57+
58+
## suppressions delete
59+
60+
Remove a suppression so Resend can send to the address again.
61+
62+
**Argument:** `<id-or-email>` — suppression ID or the suppressed email address
63+
64+
| Flag | Type | Required | Description |
65+
|------|------|----------|-------------|
66+
| `--yes` | boolean | Yes (non-interactive) | Skip confirmation |
67+
68+
**Alias:** `rm`
69+
70+
**Output:** `{"object":"suppression","id":"...","deleted":true}`
71+
72+
---
73+
74+
## suppressions batch add
75+
76+
Suppress up to 100 addresses in one request.
77+
78+
| Flag | Type | Required | Description |
79+
|------|------|----------|-------------|
80+
| `--file <path>` | string | Yes (non-interactive) | JSON file with an array of email strings (`-` for stdin) |
81+
82+
**File format:** `["a@example.com", "b@example.com"]`
83+
84+
**Output:** `{"data":[{"object":"suppression","id":"..."}]}`
85+
86+
---
87+
88+
## suppressions batch remove
89+
90+
Remove up to 100 suppressions in one request.
91+
92+
| Flag | Type | Required | Description |
93+
|------|------|----------|-------------|
94+
| `--file <path>` | string | Yes (non-interactive) | JSON file with an array of strings (`-` for stdin) |
95+
| `--ids` | boolean | No | Treat file entries as suppression IDs instead of emails |
96+
97+
**Alias:** `rm`
98+
99+
**File format:** `["a@example.com", "b@example.com"]` (or IDs with `--ids`)
100+
101+
**Output:** `{"data":[{"object":"suppression","id":"...","deleted":true}]}`

src/cli.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import { logsCommand } from './commands/logs/index';
2020
import { oauthGrantsCommand } from './commands/oauth-grants/index';
2121
import { openCommand } from './commands/open';
2222
import { segmentsCommand } from './commands/segments/index';
23+
import { suppressionsCommand } from './commands/suppressions/index';
2324
import { templatesCommand } from './commands/templates/index';
2425
import { topicsCommand } from './commands/topics/index';
2526
import { updateCommand } from './commands/update';
@@ -140,6 +141,9 @@ ${pc.gray('Examples:')}
140141
.addCommand(topicsCommand)
141142
.addCommand(domainsCommand)
142143
.addCommand(logsCommand)
144+
// Visible pre-GA with a [beta] marker in its description — the API gates usage
145+
// per account, so non-enabled users see the command but get an API error until enrolled.
146+
.addCommand(suppressionsCommand)
143147
.addCommand(apiKeysCommand)
144148
.addCommand(webhooksCommand)
145149
.addCommand(oauthGrantsCommand)

src/commands/suppressions/add.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { Command } from '@commander-js/extra-typings';
2+
import pc from 'picocolors';
3+
import { runCreate } from '../../lib/actions';
4+
import type { GlobalOpts } from '../../lib/client';
5+
import { buildHelpText } from '../../lib/help-text';
6+
import { requireText } from '../../lib/prompts';
7+
8+
export const addSuppressionCommand = new Command('add')
9+
.description('Suppress an email address so it stops receiving your emails')
10+
.argument('[email]', 'Email address to suppress')
11+
.addHelpText(
12+
'after',
13+
buildHelpText({
14+
context: `Non-interactive: <email> is required (no prompts when stdin/stdout is not a TTY).
15+
16+
A suppressed address is skipped on future sends. Added this way, the entry has
17+
origin "manual". Use "suppressions delete" to remove it again.`,
18+
output: ` {"object":"suppression","id":"<id>"}`,
19+
errorCodes: ['auth_error', 'missing_email', 'create_error'],
20+
examples: [
21+
'resend suppressions add spam@example.com',
22+
'resend suppressions add spam@example.com --json',
23+
],
24+
}),
25+
)
26+
.action(async (emailArg, _opts, cmd) => {
27+
const globalOpts = cmd.optsWithGlobals() as GlobalOpts;
28+
29+
const email = await requireText(
30+
emailArg,
31+
{ message: 'Email to suppress', placeholder: 'e.g. spam@example.com' },
32+
{
33+
message: 'Missing <email> argument.',
34+
code: 'missing_email',
35+
},
36+
globalOpts,
37+
);
38+
39+
await runCreate(
40+
{
41+
loading: 'Suppressing address...',
42+
sdkCall: (resend) => resend.suppressions.add({ email }),
43+
onInteractive: (d) => {
44+
console.log(` ${pc.gray('Email:')} ${email}`);
45+
console.log(` ${pc.gray('ID:')} ${d.id}`);
46+
},
47+
},
48+
globalOpts,
49+
);
50+
});
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import { Command } from '@commander-js/extra-typings';
2+
import type { GlobalOpts } from '../../../lib/client';
3+
import { requireClient } from '../../../lib/client';
4+
import { readFile } from '../../../lib/files';
5+
import { buildHelpText } from '../../../lib/help-text';
6+
import { outputResult } from '../../../lib/output';
7+
import { requireText } from '../../../lib/prompts';
8+
import { withSpinner } from '../../../lib/spinner';
9+
import { isInteractive } from '../../../lib/tty';
10+
import { readEmailList } from './utils';
11+
12+
export const batchAddSuppressionsCommand = new Command('add')
13+
.description(
14+
'Suppress up to 100 email addresses from a JSON file in one request',
15+
)
16+
.option(
17+
'--file <path>',
18+
'Path to a JSON file containing an array of email strings (use "-" for stdin; required in non-interactive mode)',
19+
)
20+
.addHelpText(
21+
'after',
22+
buildHelpText({
23+
context: `Non-interactive: --file
24+
Limit: 100 emails per request (API hard limit — warned if exceeded)
25+
26+
File format (--file path):
27+
["spam@example.com", "bounce@example.com"]`,
28+
output: ` {"data":[{"object":"suppression","id":"<id>"}]}`,
29+
errorCodes: [
30+
'auth_error',
31+
'missing_file',
32+
'file_read_error',
33+
'stdin_read_error',
34+
'invalid_json',
35+
'invalid_format',
36+
'create_error',
37+
],
38+
examples: [
39+
'resend suppressions batch add --file ./emails.json',
40+
'echo \'["a@example.com","b@example.com"]\' | resend suppressions batch add --file -',
41+
],
42+
}),
43+
)
44+
.action(async (opts, cmd) => {
45+
const globalOpts = cmd.optsWithGlobals() as GlobalOpts;
46+
const resend = await requireClient(globalOpts);
47+
48+
const filePath = await requireText(
49+
opts.file,
50+
{ message: 'Path to JSON file', placeholder: './emails.json' },
51+
{
52+
message:
53+
'Missing --file flag. Provide a JSON file with an array of email strings.',
54+
code: 'missing_file',
55+
},
56+
globalOpts,
57+
);
58+
59+
const raw = readFile(filePath, globalOpts);
60+
const emails = readEmailList(raw, globalOpts);
61+
62+
if (emails.length > 100) {
63+
console.warn(
64+
`Warning: ${emails.length} emails exceeds the 100-email limit. The API may reject this request.`,
65+
);
66+
}
67+
68+
const data = await withSpinner(
69+
'Suppressing addresses...',
70+
() => resend.suppressions.batch.add({ emails }),
71+
'create_error',
72+
globalOpts,
73+
);
74+
75+
if (!globalOpts.json && isInteractive()) {
76+
console.log(
77+
`Suppressed ${data.data.length} address${data.data.length === 1 ? '' : 'es'}`,
78+
);
79+
for (const entry of data.data) {
80+
console.log(` ${entry.id}`);
81+
}
82+
} else {
83+
outputResult(data, { json: globalOpts.json });
84+
}
85+
});
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { Command } from '@commander-js/extra-typings';
2+
import { buildHelpText } from '../../../lib/help-text';
3+
import { batchAddSuppressionsCommand } from './add';
4+
import { batchRemoveSuppressionsCommand } from './remove';
5+
6+
export const batchSuppressionsCommand = new Command('batch')
7+
.description('Suppress or remove many addresses in a single request')
8+
.addHelpText(
9+
'after',
10+
buildHelpText({
11+
context: `Batch operations read a JSON array of strings from --file (up to 100 per request).
12+
"add" always operates on emails. "remove" defaults to emails; pass --ids to remove by ID.`,
13+
examples: [
14+
'resend suppressions batch add --file ./emails.json',
15+
'resend suppressions batch remove --file ./emails.json',
16+
'resend suppressions batch remove --file ./ids.json --ids',
17+
],
18+
}),
19+
)
20+
.addCommand(batchAddSuppressionsCommand)
21+
.addCommand(batchRemoveSuppressionsCommand);

0 commit comments

Comments
 (0)