molted suppressions
Manage email suppressions and suppressed domains
| Command | Description |
|---|---|
molted suppressions add | Add an email to the suppression list |
molted suppressions list | List suppressions |
molted suppressions check | Check whether a send to an address (or domain) would be blocked |
molted suppressions remove | Remove a suppression by ID |
molted suppressions add-domain | Add a domain to the suppression list |
molted suppressions list-domains | List suppressed domains |
molted suppressions remove-domain | Remove a domain from the suppression list |
molted suppressions import | Bulk-import suppressions from a CSV or JSON file |
Agent context
Suppressions prevent email from being sent to specific addresses or domains. The policy engine automatically checks suppressions before every send — a suppressed contact will get status "blocked" with reason "suppressed".
Add an email suppression
molted suppressions add --email alice@example.com \
--reason hard_bounce --scope tenant
# → all future sends from your tenant to alice@example.com will be blocked
molted suppressions add --email bob@example.com \
--reason manual_dnc --scope campaign --campaign-id camp_123
# → recorded against camp_123, but currently blocks every send to
# bob@example.com from your tenant, the same as --scope tenant (#1932);
# real per-campaign scoping (blocked only for this campaign) is #1984
# With expiration (stops blocking after that date; the row stays listed):
molted suppressions add --email temp@example.com \
--reason no_engagement --scope tenant --expires-at 2027-01-01T00:00:00ZSuppression reasons
- complaint — recipient marked email as spam
- hard_bounce — address doesn't exist
- manual_dnc — do-not-contact (manually added)
- legal_request — legal/compliance requirement
- role_account — role address (admin@, info@) — avoid these
- domain_suppressed — entire domain is suppressed
- no_engagement — contact hasn't engaged (consider expiry)
Scope hierarchy
- tenant — blocked for your tenant only
- campaign - recorded per campaign, but currently blocks every send to that address from your tenant, same as scope=tenant, until real per-campaign scoping ships (#1932; tracked in #1984)
Note: platform-wide (global) suppressions cannot be created or removed via the CLI or the API (403). Automated complaint/hard-bounce handling adds tenant-scope rows (source "webhook").
Check whether a send would be blocked (use this, not list, to decide whether to send):
molted suppressions check --email alice@example.com
molted suppressions check --domain competitor.com
# → { "suppressed": true, "reason": "manual_dnc", "matchedOn": "domain" }
# Note: a campaign-scoped row reports suppressed here too (#1932), matching
# a real send -- it blocks every send to that address from your tenant,
# not just the campaign it names, until real per-campaign scoping (#1984).List and manage
molted suppressions list --email alice@example.com
molted suppressions list --limit 50 --offset 100 # page through every row
molted suppressions remove <id> # removes one row
# Removing loosens a compliance control, so it is human-gated: once the
# workspace enforces the human gate an agent key gets 403
# human_decision_required with a decisionUrl (same for remove-domain).
# It also needs an admin or all-mailbox key (list and list-domains too:
# a mailbox-scoped key gets 403 mailbox_scope_denied; check stays open).
# An address can have more than one row (tenant scope plus one or more
# campaign-scoped rows, all enforced the same way, #1932): removing this
# id does not mean the contact can receive email again if another row for
# it still exists. List every row for the address, remove each one, then
# confirm with suppressions check before sending.Domain-level suppression (block all addresses on a domain)
molted suppressions add-domain --domain spam.co
molted suppressions list-domains
molted suppressions remove-domain spam.coBulk import (migrating bounces/unsubscribes from another provider)
molted suppressions import --file bounces.csv
# CSV header: email,scope,reasonCode,campaignId,source,expiresAt
# (case-insensitive; "recipientEmail" also accepted for "email";
# campaignId, source, expiresAt are optional columns)
molted suppressions import --file bounces.json
# JSON array: [{"email":"a@x.com","scope":"tenant","reasonCode":"hard_bounce"}, ...]
molted suppressions import --file bounces.csv --dry-run # validate only, send nothing
# exits 1 if any row is invalid and a real run would fail closed (no --skip-invalid);
# exits 0 if every row is valid, or --skip-invalid is also given (a real run would succeed either way)
molted suppressions import --file bounces.csv --skip-invalid # send the valid rows, report the restEvery row is validated client-side first: email format, scope tenant|campaign (global is rejected; campaignId is required when scope is campaign -- without it the row would be silently stored as tenant-wide), reasonCode from the list above, and expiresAt (if given) must parse as a date. By default any invalid row aborts the whole import and sends nothing; pass --skip-invalid to import the valid rows anyway. Rows are sent in chunks of up to 500 rows or ~80KB of JSON, whichever comes first (the API has a ~100KB request body limit).
Caveat (#1932): a campaign-scoped suppression (scope: "campaign") is recorded against the campaign id, but currently enforced the same as --scope tenant -- it blocks every send to that address from your tenant, not just the named campaign. Real per-campaign scoping (block only that campaign's sends) is tracked in #1984. Until then, --scope campaign and --scope tenant behave the same on send; use --scope campaign only to record the campaign id for later.
Key behaviors
- Suppressions are checked automatically on every send -- no need to pre-check
- Hard bounces and complaints are auto-added by the platform (tenant scope)
- Once no active row (email or domain) matches, the suppression stops blocking sends to that address immediately
Related: molted consent, molted send, molted analytics
Commands
molted suppressions add
Add an email to the suppression list
molted suppressions add [options]| Option | Description | Default |
|---|---|---|
--email <email> | Email address to suppress (required) (required) | |
--reason <code> | Reason code (required; one of: complaint, hard_bounce, manual_dnc, legal_request, role_account, domain_suppressed, no_engagement) | |
--scope <scope> | Scope (required; one of: tenant, campaign) | |
--campaign-id <id> | Campaign ID recorded with --scope campaign (ignored for tenant scope; not required by the CLI or API, though import requires it) | |
--source <source> | Source of the suppression (free text; "portal_keep_away" is reserved, 400) | |
--expires-at <iso> | Expiration date (ISO 8601) |
molted suppressions list
List suppressions
molted suppressions list [options]| Option | Description | Default |
|---|---|---|
--email <email> | Filter by email address | |
--limit <n> | Page size, 1-500 (default: every matching row, or the latest 100 with no filter; 100 when --offset is given) | |
--offset <n> | Rows to skip, 0-1,000,000 (default 0) |
molted suppressions check
Check whether a send to an address (or domain) would be blocked
molted suppressions check [options]| Option | Description | Default |
|---|---|---|
--email <email> | Address to check (exactly one of --email/--domain) | |
--domain <domain> | Domain to check: is every address at it suppressed? |
Answers with the same check the policy engine runs on every send: email and domain suppressions, tenant and platform-wide rows, and expiry. An expired row reads as clear; a domain row counts for every address at it. Note: a campaign-scoped row reports suppressed here too (#1932) -- it currently blocks every send to that address from your tenant, not just the campaign it names, until real per-campaign scoping ships (#1984).
molted suppressions check --email bob@example.com
# → { "email": "bob@example.com", "suppressed": true, "reason": "manual_dnc", "matchedOn": "domain" }
molted suppressions check --domain competitor.com
# → { "domain": "competitor.com", "suppressed": false, "reason": null, "matchedOn": null }If the lookup itself fails, the API answers 503 rather than "clear" -- don't send until it answers. This command always exits 0 for a normal answer (suppressed or not); only a request error (bad params, 503) exits non-zero.
Related: molted suppressions add, molted suppressions list, molted consent check
molted suppressions remove
Remove a suppression by ID
molted suppressions remove <id>| Argument | Description | Required |
|---|---|---|
<id> | yes |
molted suppressions add-domain
Add a domain to the suppression list
molted suppressions add-domain [options]| Option | Description | Default |
|---|---|---|
--domain <domain> | Domain to suppress (required) (required) | |
--reason <code> | Reason code (one of: complaint, hard_bounce, manual_dnc, legal_request, role_account, domain_suppressed) | manual_dnc |
--source <source> | Source of the suppression |
molted suppressions list-domains
List suppressed domains
molted suppressions list-domains [options]| Option | Description | Default |
|---|---|---|
--limit <n> | Page size, 1-500 (default: every row; 100 when --offset is given) | |
--offset <n> | Rows to skip, 0-1,000,000 (default 0) |
molted suppressions remove-domain
Remove a domain from the suppression list
molted suppressions remove-domain <domain>| Argument | Description | Required |
|---|---|---|
<domain> | yes |
molted suppressions import
Bulk-import suppressions from a CSV or JSON file
molted suppressions import [options]| Option | Description | Default |
|---|---|---|
--file <path> | Path to a .csv (header row) or .json (array) file (required) (required) | |
--skip-invalid | Import the valid rows even if some rows fail validation | |
--dry-run | Validate only; do not send anything |
See CLI overview for global options, credentials and output format.