[Email Service] Document account-wide suppressions - #32948
Conversation
Review
👉 Fix in your agent 👈Fix the following review findings in PR #32948 (https://github.com/cloudflare/cloudflare-docs/pull/32948).
Before making changes, review each finding and present a brief summary table:
- For each finding, state whether you agree, disagree, or need clarification
- If you disagree (e.g. the fix requires disproportionate effort for minimal benefit,
or the finding is factually incorrect), explain why
- If you need clarification before deciding, ask those questions
- Then share your plan for which issues to tackle and in what order
After triaging, follow this order:
1. Post a comment on this PR for any findings you are skipping, with the finding ID and your reasoning.
2. Then commit the fixes for the legitimate findings.
The comment must come before the commit — the bot reads PR comments when a new
push triggers a review, so skip comments posted after the push will be missed.
---
## Code Review
### Warnings (1)
#### CR-f5dc835d3bd1 · PR scope mismatch
- **File:** `src/content/docs/dns/nameservers/nameserver-options.mdx` line 25
- **Issue:** The PR title is '[Email Service] Document account-wide suppressions', but the changed file is src/content/docs/dns/nameservers/nameserver-options.mdx and the additions describe Cloudflare DNS nameserver assignment behavior, not email suppressions.
- **Fix:** Verify this is the intended file/branch for the PR. If these DNS nameserver changes are unrelated to the PR's stated purpose, move them to the correct PR or update the PR title and description.
---
## Conventions
### Warnings (1)
#### CV-b617070ce148 · Scope accuracy
- **File:** PR-level finding
- **Issue:** The description only covers Email Service suppression documentation, but the diff touches ~770 files across many unrelated product areas (Cloudflare WAN, Cloudflare One, Workers, AI Search, Radar, WAF, model catalogs, changelogs) and removes src/content/docs/cloudflare-one/traffic-policies/network-policies/ssh-logging.mdx plus dozens of partials — none of which are mentioned.
- **Fix:** Describe the full scope of the diff in the PR description, or rebase the branch so the diff contains only the Email Service changes.
Code ReviewThis code review is in beta and may not always be helpful — use your judgment. Warnings (1)
ConventionsWarnings (1)
Style Guide ReviewNo style-guide issues found. CommandsOnly codeowners can run commands. Post a comment with the command to trigger it.
|
|
This pull request requires reviews from CODEOWNERS as it changes files that match the following patterns:
|
🚀 Deploying Preview to Cloudflare 🚀Preview URL: https://ruisilva-email-2041.previews.developers.cloudflare.com (commit 844e126)This URL reflects your latest Preview deploymentPreview Deployments by commit
|
|
|
||
| When some recipients are suppressed, Email Service removes them and continues processing the remaining recipients. When every recipient is suppressed, the server returns `250 2.0.0 Ok` but does not deliver the message. | ||
|
|
||
| Use [Email sending logs](/email-service/observability/logs/) to confirm delivery. Suppressed recipients appear with a **Rejected** result. |
|
|
||
| Search with a complete address or a username. A username search checks that username across every domain. For example, `alex` matches `alex@example.com` and `alex@example.org`. | ||
|
|
||
| ## 2. Add a suppression in the dashboard |
There was a problem hiding this comment.
Should we mention the import button and supported formats here?
| --- | ||
|
|
||
| Suppression lists prevent emails from being sent to addresses that should not receive them, protecting your sender reputation and ensuring compliance with anti-spam regulations. | ||
| An Email Sending suppression list contains recipients that Email Service does not contact. Suppressions protect your sender reputation and help prevent unwanted mail. |
There was a problem hiding this comment.
| An Email Sending suppression list contains recipients that Email Service does not contact. Suppressions protect your sender reputation and help prevent unwanted mail. | |
| An Email Sending suppression list contains recipients that Email Service does not contact. Suppressions protect your sender reputation and help prevent sending unwanted mail. |
| Cloudflare creates suppressions after eligible delivery failures and spam complaints. You can create manual suppressions for recipients who should not receive your mail. The management API can also return Cloudflare-managed policy entries. | ||
|
|
||
| Cloudflare automatically manages suppressions for your account to preserve your reputation as an email sender. | ||
| To add or remove entries, refer to [Manage suppressions](/email-service/configuration/suppressions/). |
There was a problem hiding this comment.
| To add or remove entries, refer to [Manage suppressions](/email-service/configuration/suppressions/). | |
| To add or remove entries, refer to [Manage suppressions](/email-service/configuration/suppressions/). Suppressions only apply to Email Sending. |
| - **Hard bounces**: Invalid or non-existent email addresses are immediately suppressed. | ||
| - **Repeated soft bounces**: Addresses that repeatedly fail delivery are temporarily or permanently suppressed based on the frequency and pattern of failures. | ||
| - **Spam complaints**: Recipients who marked emails as spam. Cloudflare integrates with Postmasters to receive spam complaints and automatically updates your account suppression list to prevent you from sending emails to this email address and preserve your email sending reputation. | ||
| The Email Sending dashboard and suppression management API are account-scoped. New entries created through these interfaces apply to every sending domain and subdomain in the account. |
There was a problem hiding this comment.
| The Email Sending dashboard and suppression management API are account-scoped. New entries created through these interfaces apply to every sending domain and subdomain in the account. | |
| Suppressions are account-scoped. Once an email address is on the suppression list, sending to that email address from any of the domains within your account will be suppressed. | |
| If you need to have different suppression lists for different use cases, consider using separate Cloudflare accounts. |
| The Email Sending dashboard and suppression management API are account-scoped. New entries created through these interfaces apply to every sending domain and subdomain in the account. | ||
|
|
||
| You may also manually add or remove email addresses from your suppression list as needed. The removal of email addresses that have been automatically added to your suppression list as a result of a spam complaint is limited to avoid abuse. | ||
| These interfaces do not let you create zone-scoped or domain-scoped entries. Each recipient can have only one active Email Sending suppression per account. |
There was a problem hiding this comment.
| These interfaces do not let you create zone-scoped or domain-scoped entries. Each recipient can have only one active Email Sending suppression per account. |
| | Reason | Created when | Expiration | Customer action | | ||
| | ------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------- | --------------------------------------------------------------------- | | ||
| | Manual (`manual`) | You add the recipient through the dashboard or API | The time you choose, or no expiration | Change the expiration or delete the entry | | ||
| | Spam complaint (`complaint`) | Cloudflare receives and validates a complaint from the recipient's email provider | Created without an expiration | Delete only after the recipient provides new consent | |
There was a problem hiding this comment.
Delete only after the recipient provides new consent
What does this mean?
There was a problem hiding this comment.
Applies to all other rows
| | ------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------- | --------------------------------------------------------------------- | | ||
| | Manual (`manual`) | You add the recipient through the dashboard or API | The time you choose, or no expiration | Change the expiration or delete the entry | | ||
| | Spam complaint (`complaint`) | Cloudflare receives and validates a complaint from the recipient's email provider | Created without an expiration | Delete only after the recipient provides new consent | | ||
| | Hard bounce (`hard_bounce`) | The receiving system permanently rejects an eligible recipient | 7 days by default | Wait for expiration, or delete only after verifying the address | |
There was a problem hiding this comment.
| | Hard bounce (`hard_bounce`) | The receiving system permanently rejects an eligible recipient | 7 days by default | Wait for expiration, or delete only after verifying the address | | |
| | Hard bounce (`hard_bounce`) | The receiving system permanently rejects an eligible recipient | 7 days | Wait for expiration, or delete only after verifying the address | |
| | Manual (`manual`) | You add the recipient through the dashboard or API | The time you choose, or no expiration | Change the expiration or delete the entry | | ||
| | Spam complaint (`complaint`) | Cloudflare receives and validates a complaint from the recipient's email provider | Created without an expiration | Delete only after the recipient provides new consent | | ||
| | Hard bounce (`hard_bounce`) | The receiving system permanently rejects an eligible recipient | 7 days by default | Wait for expiration, or delete only after verifying the address | | ||
| | Confirmed invalid recipient (`hard_bounce`) | All observed failures confirm an invalid recipient, or qualifying availability failures persist without a successful delivery | No expiration | Delete only after independently verifying the address | |
There was a problem hiding this comment.
Confirmed invalid recipient
What does this mean in english? Inbox does not exist?
| | Hard bounce (`hard_bounce`) | The receiving system permanently rejects an eligible recipient | 7 days by default | Wait for expiration, or delete only after verifying the address | | ||
| | Confirmed invalid recipient (`hard_bounce`) | All observed failures confirm an invalid recipient, or qualifying availability failures persist without a successful delivery | No expiration | Delete only after independently verifying the address | | ||
| | Soft bounce (`soft_bounce`) | An eligible recipient-side temporary failure occurs, such as a full mailbox or rate limit | 24 hours by default | Wait for expiration, or delete after confirming the issue is resolved | | ||
| | Cloudflare-managed metadata (`policy`) | The API returns a policy entry with `read_only: true` | The displayed expiration, or no expiration | Contact [Cloudflare Support](/support/contacting-cloudflare-support/) | |
There was a problem hiding this comment.
The API returns a policy entry with
read_only: true
What API? Is it when trying to send email? Is it the binding?
There was a problem hiding this comment.
Consider omitting this row entirely
|
|
||
| ### Manual suppressions | ||
|
|
||
| Manual suppressions support application-level decisions that Email Service cannot observe. For example, add a manual suppression when a recipient unsubscribes. |
There was a problem hiding this comment.
| Manual suppressions support application-level decisions that Email Service cannot observe. For example, add a manual suppression when a recipient unsubscribes. | |
| Manual suppressions allow you to add application-level decisions that Email Service cannot observe. For example, if a user manually unsubcribes from emails in your app, you can add their email to your Email Service suppression list. |
| Email providers send feedback reports when recipients mark messages as spam. Cloudflare validates these reports before creating complaint suppressions. | ||
|
|
||
| Complaint suppressions are created without an expiration. Delete or shorten one only after the recipient provides new consent, such as a fresh opt-in. |
There was a problem hiding this comment.
Delete or shorten one only after the recipient provides new consent, such as a fresh opt-in.
This is fully at the user discretion right? "Fresh opt-in within your app" could be more clear
| ## Suppression timing | ||
|
|
||
| Bounce suppressions rely on background delivery processing and are not immediate. Messages already being delivered can fail before the suppression takes effect. | ||
|
|
||
| Complaint suppressions appear after Cloudflare receives and validates the provider report. The provider determines when that report arrives. | ||
|
|
||
| Expired entries stop appearing in the public list when their `expires_at` timestamp passes. Delivery enforcement can take additional time to expire. Updates and deletions also propagate asynchronously, so the management list and delivery enforcement can briefly differ. |
There was a problem hiding this comment.
| ## Suppression timing | |
| Bounce suppressions rely on background delivery processing and are not immediate. Messages already being delivered can fail before the suppression takes effect. | |
| Complaint suppressions appear after Cloudflare receives and validates the provider report. The provider determines when that report arrives. | |
| Expired entries stop appearing in the public list when their `expires_at` timestamp passes. Delivery enforcement can take additional time to expire. Updates and deletions also propagate asynchronously, so the management list and delivery enforcement can briefly differ. |
I think we can omit this entirely. Too many docs means less reading
| ### List suppressions | ||
|
|
||
| List active suppressions for the account: | ||
|
|
||
| <APIRequest | ||
| path="/accounts/{account_id}/email/sending/suppressions" | ||
| method="GET" | ||
| parameters={{ per_page: 50 }} | ||
| /> | ||
|
|
||
| Each entry contains the following fields: | ||
|
|
||
| | Field | Description | | ||
| | ------------ | --------------------------------------------------------------------------- | | ||
| | `id` | Unique suppression identifier | | ||
| | `email` | Suppressed recipient | | ||
| | `reason` | `manual`, `complaint`, `hard_bounce`, `soft_bounce`, or `policy` | | ||
| | `created_at` | Creation timestamp | | ||
| | `expires_at` | Expiration timestamp, or `null` when the entry has no expiration | | ||
| | `read_only` | Whether the server prevents customers from updating or deleting the entry | | ||
|
|
||
| The account list includes active Email Sending suppressions owned by the account. The API does not return zone or domain scope. New entries created through this API apply account-wide. | ||
|
|
||
| ### Get one suppression | ||
|
|
||
| Retrieve one suppression by ID: | ||
|
|
||
| <APIRequest | ||
| path="/accounts/{account_id}/email/sending/suppressions/{suppression_id}" | ||
| method="GET" | ||
| /> | ||
|
|
||
| The item response also includes its optional `note` field. | ||
|
|
||
| ### Find an exact address | ||
|
|
||
| Use the `email` parameter to check one complete address: | ||
|
|
||
| <APIRequest | ||
| path="/accounts/{account_id}/email/sending/suppressions" | ||
| method="GET" | ||
| parameters={{ email: "recipient@example.com" }} | ||
| /> | ||
|
|
||
| An empty `result` array means the account has no active entry for that address. | ||
|
|
||
| ### Search by username | ||
|
|
||
| The `search` parameter accepts a complete address or a username ending in `@`. A username search matches that username across every domain: | ||
|
|
||
| <APIRequest | ||
| path="/accounts/{account_id}/email/sending/suppressions" | ||
| method="GET" | ||
| parameters={{ search: "alex@" }} | ||
| /> | ||
|
|
||
| Search is case-insensitive. Values such as `alex` or `@example.com` are invalid API searches. The dashboard automatically adds the trailing `@` for username searches. | ||
|
|
||
| ### Filter by reason | ||
|
|
||
| Use the `reason` parameter to filter API results: | ||
|
|
||
| <APIRequest | ||
| path="/accounts/{account_id}/email/sending/suppressions" | ||
| method="GET" | ||
| parameters={{ reason: "complaint" }} | ||
| /> | ||
|
|
||
| Valid values are `manual`, `complaint`, `hard_bounce`, `soft_bounce`, and `policy`. | ||
|
|
||
| ### Paginate results | ||
|
|
||
| The list uses cursor pagination. Read `result_info.next_cursor` and pass it as `cursor`: | ||
|
|
||
| <APIRequest | ||
| path="/accounts/{account_id}/email/sending/suppressions" | ||
| method="GET" | ||
| parameters={{ per_page: 50, cursor: "$NEXT_CURSOR" }} | ||
| /> | ||
|
|
||
| Continue until `next_cursor` is `null`. Large, filtered lists can return a short or empty intermediate page with another cursor. Do not treat an empty page as the end while a cursor remains. | ||
|
|
||
| The cursor contains the search and reason filters. Keep those filters unchanged while paginating. To change a filter, start again without a cursor. | ||
|
|
||
| The response does not include a total count. | ||
|
|
||
| ### Update a suppression | ||
|
|
||
| Use `PATCH` to update the expiration or note: | ||
|
|
||
| <APIRequest | ||
| path="/accounts/{account_id}/email/sending/suppressions/{suppression_id}" | ||
| method="PATCH" | ||
| json={{ | ||
| expires_at: null, | ||
| note: "Recipient requested a permanent opt-out", | ||
| }} | ||
| /> | ||
|
|
||
| Set `expires_at` to a future ISO 8601 timestamp for a temporary entry. Set it to `null` for no expiration. Omitting a field leaves its current value unchanged, and an empty string clears the note. | ||
|
|
||
| The API returns `403` when `read_only` is `true`. Extending an automatic suppression can provide additional protection. Shorten or remove one only after verifying the recipient. | ||
|
|
||
| ### Remove a suppression | ||
|
|
||
| Delete a mutable suppression by ID: | ||
|
|
||
| <APIRequest | ||
| path="/accounts/{account_id}/email/sending/suppressions/{suppression_id}" | ||
| method="DELETE" | ||
| /> | ||
|
|
||
| The API returns `403` for read-only suppressions. Updates and deletions propagate asynchronously, so the management list and delivery enforcement can briefly differ. | ||
|
|
||
| :::caution | ||
| Deleting a hard-bounce or complaint suppression permits delivery attempts to an address that already failed or reported your mail as spam. Verify the recipient before deleting the entry. | ||
| ::: | ||
|
|
||
| ## 5. Verify suppression changes | ||
|
|
||
| Refresh the suppression list after creating or updating an entry. Confirm that the recipient, reason, and expiration match the intended values. | ||
|
|
||
| After deleting an entry, confirm that it no longer appears. Delivery enforcement can briefly lag behind the management list. | ||
|
|
||
| ## 6. Limits | ||
|
|
||
| For page size, bulk import, and rate limits, refer to [Suppression list limits](/email-service/platform/limits/#suppression-list-limits). |
There was a problem hiding this comment.
Too much docs. This doc does not need to explain how to use each endpoint. Can add link to Wrangler and API, or use tabs to show how to add remove for each. But this doc is too long
|
|
||
| When the setting is off, the REST API returns `400`, the Workers binding throws `E_RECIPIENT_SUPPRESSED`, and SMTP rejects the message. Any suppressed recipient causes the send to fail. | ||
|
|
||
| When the setting is on, Email Service removes suppressed recipients and processes the remaining recipients. If none remain, SMTP may return `250 2.0.0 Ok` without a Message-ID and deliver nothing. |
There was a problem hiding this comment.
Might be worth clarifying that if all recipients are suppressed, RPC & HTTP will still throw that error
- Removes references to the "Applied Learned Schema" functionality removed from the dashboard - Removes references to the `cf-risk-missing-schema` label as this is no longer applied.
…ltimekit (#32946) * docs(realtimekit): added release notes for 24 aug 2026 release of realtimekit * docs(realtimekit): added the new localization key in language-pack * docs(realtimekit): removed internal fix line
* Add Logpush Transformers documentation (closed beta) * Resolve VERIFY markers in Logpush Transformers doc * Rename to Transformers, add Beta badge, clarify Studio * Remove API reference link (hidden during closed beta)
… downloads (#32915) * Document self-serve Cloudflare One Virtual Appliance downloads Update the connector configuration partial so the Cloudflare One Virtual Appliance is documented as self-serve across VMware ESXi (OVA image), Proxmox (VM helper script), and a new libvirt/KVM (beta) tab. License keys are now generated self-serve from the Connectors page. Co-authored-by: OpenCode <noreply@opencode.ai> Co-authored-by: Build <noreply@cloudflare.com> Co-authored-by: Anthropic <noreply@anthropic.com> Co-authored-by: claude-opus-4-8 <noreply@anthropic.com> * VMWare > VMware * Add changelog for self-serve virtual appliance download Announce selecting a hypervisor (VMware ESXi, Proxmox, libvirt/KVM) and downloading the Cloudflare One Virtual Appliance image or install script directly from the Connectors page. Co-authored-by: OpenCode <noreply@opencode.ai> Co-authored-by: Build <noreply@cloudflare.com> Co-authored-by: Anthropic <noreply@anthropic.com> Co-authored-by: claude-opus-4-8 <noreply@anthropic.com> --------- Co-authored-by: OpenCode <noreply@opencode.ai> Co-authored-by: Build <noreply@cloudflare.com> Co-authored-by: Anthropic <noreply@anthropic.com>
* Clarify that assigned nameservers cannot be changed DEE-3776 Common customer question: 'why did my new zone get different nameservers than my other zones, can we reassign it?' The answer is already documented but easy to miss. Small changes to make it more discoverable: - DNS FAQ: new entry 'Why does my new zone have different nameservers than my other zones?' explaining the reasons a new zone can be assigned a different pair (cross-account conflict, prior deletion, parent/child rules, Foundation DNS color rotation) and how to get consistent nameservers via Account custom nameservers or DNS zone defaults. - Nameserver options: replace a plain sentence with a callout that points to the new FAQ entry and to the Nameserver consistency section. - Foundation DNS advanced nameservers: new subsection under Nameservers hosting and assignment that explains why a new zone might get a different color set (blue / gold / orange) and cross-links Account custom nameservers. * Style guide: remove directional word from Nameserver consistency link * Address review feedback from @hannes-cf - faq.mdx: reword 'a different pair' -> 'different nameservers'; drop 'pair' in the parent/child bullet; use 'set' consistently in the delete-and-re-add caveat. - foundation-dns/advanced-nameservers.mdx: 'The same name is' -> 'The same domain is', matching the FAQ wording. - nameservers/nameserver-options.mdx: replace titled ':::note[...]' with an untitled ':::caution' (per docs convention and given severity); in the adjacent pre-existing caution, replace 'a new pair of nameservers' with 'a new set of nameservers' for wording consistency. Also rebased onto origin/production.
Document post-quantum key agreement on inbound and outbound TLS 1.3 SMTP connections between Cloudflare Email Security MX deployments and third-party mail servers. Split out of #32966 per review feedback.
* fix: serve original images in local dev The @astrojs/cloudflare/image-service emits /cdn-cgi/image/ URLs that only exist on Cloudflare's edge, so every image 404s under astro dev. Use the passthrough service locally (original files are already local, no resizing needed) and keep the Cloudflare service for production builds. * fix: trim image service dev-mode comment
Summary
Documents account-wide Email Sending suppressions and adds a dashboard and REST API management guide. Clarifies suppression reasons, expiration behavior, read-only policy metadata, limits, troubleshooting, and REST API, Workers binding, and SMTP behavior.
Documentation checklist