MOLTED EMAIL
CLI Reference

molted suppressions

Manage email suppressions and suppressed domains

CommandDescription
molted suppressions addAdd an email to the suppression list
molted suppressions listList suppressions
molted suppressions checkCheck whether a send to an address (or domain) would be blocked
molted suppressions removeRemove a suppression by ID
molted suppressions add-domainAdd a domain to the suppression list
molted suppressions list-domainsList suppressed domains
molted suppressions remove-domainRemove a domain from the suppression list
molted suppressions importBulk-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:00Z

Suppression 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.co

Bulk 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 rest

Every 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]
OptionDescriptionDefault
--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]
OptionDescriptionDefault
--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]
OptionDescriptionDefault
--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>
ArgumentDescriptionRequired
<id>yes

molted suppressions add-domain

Add a domain to the suppression list

molted suppressions add-domain [options]
OptionDescriptionDefault
--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]
OptionDescriptionDefault
--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>
ArgumentDescriptionRequired
<domain>yes

molted suppressions import

Bulk-import suppressions from a CSV or JSON file

molted suppressions import [options]
OptionDescriptionDefault
--file <path>Path to a .csv (header row) or .json (array) file (required) (required)
--skip-invalidImport the valid rows even if some rows fail validation
--dry-runValidate only; do not send anything

See CLI overview for global options, credentials and output format.