Connect Cloudflare
Send transactional email through Cloudflare Email Service and receive it with Email Routing, in your own Cloudflare account. Two API tokens, what Stamp0 deploys, limits, costs and the transactional-only rule.
A Cloudflare connection is one Cloudflare account. Like an AWS connection, it belongs to your organization and any project can use it, and you choose per domain which connection sends and which receives: acme.com can send through Cloudflare while news.acme.com sends through SES.
Beta
The Cloudflare provider is in beta for every workspace, because Cloudflare Email Service sending is in public beta at Cloudflare (Email Routing, which receives, is generally available). Cloudflare can change the sending API, its limits and its pricing before general availability. The limits on this page still apply: transactional mail only, at most 2 emails per second per connection, and a daily quota that starts around 1,000 emails on a new Cloudflare account.
Before you start
- Workers Paid. Cloudflare Email Service sending needs the Workers Paid plan ($5 a month), and Stamp0 requires it for every Cloudflare connection, including receive-only ones. Verification fails with
cloudflare_plan_requiredwithout it. - Cloudflare DNS. Each domain's zone must use Cloudflare DNS, for sending and for receiving. A domain whose DNS is elsewhere answers
422 zone_not_on_cloudflare. - Transactional email only. See Transactional only before you connect: Cloudflare can disable sending for your whole account if it sees bulk mail.
- An owner or admin. Connections are created and changed in the dashboard only. API keys, the CLI and MCP can read a connection's status but never hold a Cloudflare token.
Two tokens
The token Stamp0 keeps cannot deploy code. Changes to what runs in your account use a second token that you supply each time and that Stamp0 never keeps.
The setup token (never kept)
Used to deploy and change Stamp0's resources in your account: the first deploy, upgrades, rollbacks after the watch window, redeploys after a change outside Stamp0, adding or removing a sending domain, turning receiving on or off for a name, key rotation, and removal. Stamp0 holds it sealed for at most one hour while a deploy runs as a background job (never in the job itself), then deletes it; for a domain change it is used for that one request only. Give it a short expiry when you create it.
| Permission | Scope | Why |
|---|---|---|
| Workers Scripts Write | Account | Deploy, upgrade and roll back the Stamp0 Worker; event subscriptions |
| Workers R2 Storage Write | Account | Create the mail bucket and its retention rule |
| Queues Write | Account | Create the queue, its dead-letter queue and event subscriptions |
| Email Sending Write | Account | Onboard and remove sending domains, turn email preview off. Add it by hand (below) |
| Zone Settings Write | The zones you send or receive on | Turn Email Routing on and off for a receiving name, and set subaddressing |
| Email Routing Rules Write | The zones you receive on | Point the apex catch-all at the Worker (apex_catch_all), and remove Stamp0's rules when receiving is turned off or the connection is removed. Add it by hand |
| DNS Write | The zones you send or receive on, and the zones of Amazon SES domains you publish on Cloudflare | Read your apex MX, SPF and DMARC before and after enabling Routing, delete the apex SPF Cloudflare adds when you had none, and roll back any other change. Publish an Amazon SES domain's records when you ask (below). When you delete a domain, remove the records Stamp0 published and the DMARC p=reject Cloudflare created at onboarding, each only while unchanged |
| Zone Read | The zones you send or receive on, and those SES zones | Find zones and read Email Routing status |
| Account Analytics Read | Account | Read the Stamp0 Worker's error rate during the 30-minute watch window after a deploy, when the runtime token cannot (Versions and upgrades) |
Stamp0 never creates API tokens, so it does not need Account API Tokens Write, and it never uses Email Routing destination addresses (rules go to the Worker), so it does not need Email Routing Addresses. When Cloudflare refuses a setup-token call, the API answers 422 validation_error with setup_token_permissions: and the permission the call needed.
The runtime token (stored, sealed)
Used for everything day to day: sending, pulling events and inbound pointers from the queue, reading stored mail, inbox rules and verification.
| Permission | Scope | Why |
|---|---|---|
| Email Sending Write | Account | Send, keep Cloudflare's suppression list in step, read the daily quota and reputation. Add it by hand (below) |
| Email Routing Rules Write | The zones you choose | Add and remove a rule per inbox. Add it by hand |
| Zone Read | The zones you choose | Find zones |
| Zone Settings Read | The zones you receive on | Read whether Email Routing is ready on each receiving name, and its records (Zone Read alone cannot). Runtime tokens created before October 20, 2026 lack it: add it (below), or receiving names stay setup_token_required |
| Queues Write | Account | Pull and acknowledge messages from Stamp0's queue (Cloudflare has no pull-only permission) |
| Workers R2 Storage Bucket Item Write | The stamp0-<id> bucket only | Read and write stored mail. Add it by hand (below) |
| Workers Scripts Read | Account | Check the deployed Worker version for drift; it cannot deploy |
| Account Analytics Read | Account, optional | Read what the whole Cloudflare account sent today against the daily quota, mail sent outside Stamp0 included (Sending), and the Worker's error rate after a deploy |
When you verify, Stamp0 checks every permission with a read call and shows a named checklist, and checks that the runtime token cannot upload a Worker: a token that could is refused. Replacing the token (Replace runtime token) runs the same checks before the swap. If Cloudflare rejects it later, the connection shows credential_invalid.
When a re-check finds a permission missing (other than Zone Settings Read alone, below), the connection goes to error with last_error permission_missing: and the checks that failed (permission_missing:queues,r2_bucket), and the dashboard names them.
Adding Zone Settings Read to an older token
A runtime token created before October 20, 2026 has no Zone Settings Read. It keeps sending: verification leaves the connection connected and adds the notice runtime_token_permission_missing with detail zone_settings_read, but receiving names stay setup_token_required because Stamp0 cannot read Email Routing's status. To fix it:
- In the Cloudflare dashboard, open My Profile, then API Tokens, and Edit the Stamp0 runtime token.
- Add Zone, Zone Settings, Read, and include the zones you receive on.
- Save. Cloudflare keeps the token's value, so there is nothing to replace in Stamp0.
- Re-verify the connection (or wait for the next re-check). The notice goes away.
Permissions the link cannot pre-select
The dashboard's Create the token in Cloudflare links open Cloudflare's token form with the token named and every permission Cloudflare has a template key for already selected. Cloudflare publishes no key for Email Sending or Email Routing Rules (and refuses a link that asks for one it does not recognize), so next to each link the dashboard shows Add these in Cloudflare before creating the token, with the exact rows to add:
| Token | Permission | Resource | Access | Scope |
|---|---|---|---|---|
| Setup | Email Sending Write | Account | Edit | Your account |
| Setup | Email Routing Rules Write | Zone | Edit | The zones you receive on |
| Runtime | Email Sending Write | Account | Edit | Your account |
| Runtime | Email Routing Rules Write | Zone | Edit | The zones you receive on |
| Runtime | Workers R2 Storage Bucket Item Write | Account | Edit | The stamp0-<id> bucket only |
When the token is for a domain you are adding, the dashboard names that domain's zone. A template link also cannot pick zones or buckets, so set the zone and bucket scope of every row in the form, as the tables above say.
R2 is added by hand on purpose. Cloudflare's template has only one R2 key, and it pre-selects Workers R2 Storage Write on the whole account, which would let the stored token read and write every bucket you have; a template also cannot pick a bucket. So the runtime link leaves R2 out: in Cloudflare's token form, add Workers R2 Storage Bucket Item Write yourself and choose the stamp0-<id> bucket the dialog names. For the same reason Stamp0 never hands out presigned R2 links (Cloudflare mints their credentials only for account-wide R2 tokens): attachment downloads go through Stamp0 (see Sending).
Connecting
- In the dashboard, choose Cloudflare, enter your account ID, and open the setup-token link. Cloudflare's token page opens with the permissions pre-selected where Cloudflare has a template key. Add the ones the dialog lists under Add these in Cloudflare before creating the token (above), set each row's zones, and compare with the full list the dialog shows.
- Tick the acknowledgment: Cloudflare Email Service is transactional only. If Cloudflare sees bulk mail it can disable sending for the whole account, including mail you send outside Stamp0. It is recorded in your audit log.
- Paste the setup token. Stamp0 deploys its data plane (below); then you create the runtime token from a second link that names the bucket, with exactly the bucket scope.
- Verification runs and shows the checklist. The connection is then
connected.
What Stamp0 deploys in your account
Every name ends in a short id derived from your connection (s0…), and every id is recorded on the connection. Changes and removal only ever touch what is recorded there.
| Resource | Name | Notes |
|---|---|---|
| R2 bucket | stamp0-<id> | Inbound mail and outbound content. Deleted by age after the retention period (30 days by default, never under 21). Kept when you remove the connection |
| Queue | stamp0-<id> | Inbound pointers and sending events. 14-day retention, up to 100 delivery attempts |
| Dead-letter queue | stamp0-<id>-dlq | Drained automatically every few minutes |
| Worker | stamp0-<id> | Receives mail from Email Routing and stores it (below). No npm dependencies, no MIME parsing |
| Event subscriptions | one per sending domain | Delivery events to the queue; created before the domain can send |
| Email Routing rules | stamp0:<id>:<inbox> | One per inbox address. Stamp0 never edits or deletes a rule without its own prefix |
| Catch-all rule | stamp0:<id>:catch_all | Only with apex_catch_all; the rule it replaced is recorded and put back |
Versions and upgrades
The Worker is built reproducibly and signed; Stamp0 refuses to deploy a bundle whose hash or signature does not match the release. Stamp0 never upgrades it on its own: the connection shows data_plane.status (up_to_date, update_available, update_required for security fixes and major versions, or modified when the Worker was changed outside Stamp0), and an owner or admin applies the change with a setup token. After that, Roll back is one click with a setup token.
The watch window. For 30 minutes after an upgrade or redeploy, while the setup token is still held, Stamp0 checks the new version every minute and rolls back to the previous one automatically when:
- more than 1% of the Worker's invocations throw (from at least 20), read from Cloudflare's Workers analytics with the runtime token, or with the setup token when the runtime token has no Account Analytics Read; with neither, only the next check applies;
- any message the new version stores fails Stamp0's signature check.
The rollback shows on the connection (data_plane_rolled_back_exception_rate or data_plane_rolled_back_bad_pointer). A redeploy right after someone edited the Worker in Cloudflare's dashboard keeps the last version Stamp0 deployed as the one to roll back to, never your edit. The first deploy has nothing to roll back to, so it has no watch window.
Pinning. An owner or admin can keep a connection on one version: PUT /connections/:id/data-plane/pin with { "version": "1.0.1" } (or no body for the deployed version), or { "version": "opt_out" } to refuse every upgrade. A pinned connection answers 409 conflict to an upgrade, redeploy or key rotation onto any other version (with opt_out, to every one) before anything changes; rollback still works. DELETE /connections/:id/data-plane/pin removes the pin. The connection shows it as data_plane.pinned and data_plane.pin, and both changes are in the audit log. Pinning deploys nothing. A version superseded by a security fix cannot be pinned.
Deploy pause (kill switch). Stamp0 can pause every data-plane deploy and upgrade across all connections, for example during an incident. While deploys are paused, new Cloudflare connections answer provider_not_configured, and POST /connections/:id/data-plane answers 409 conflict for everything except rollback, which restores a version you already approved. Sending, receiving and pulls keep running.
Removing the connection
With a setup token, Stamp0 deletes its Worker, rules, event subscriptions and queues, puts back an apex catch-all and subaddressing setting it changed, and turns Email Routing off on a name only if Stamp0 turned it on and no other rule uses it. The R2 bucket is kept (it holds your mail) and the dashboard says so. Without a setup token, the connection is removed from Stamp0 and you get the exact list to delete by hand. While domains still use it, the connection stays disconnected and its data_plane.status is destroyed, with no version, since Stamp0 no longer runs anything in the account for it.
Domains
Cloudflare creates and locks the DNS records for a Cloudflare domain; Stamp0 writes none of them and you should not edit them. Enable on Cloudflare (with a setup token) onboards the domain:
- DMARC. Cloudflare never overwrites an existing
_dmarcrecord. When there is none, it createsv=DMARC1; p=reject;, which rejects any of your other mail for that domain that is not aligned. Stamp0 needs your confirmation first (confirm_dmarc_create): without it,POST /domainsanswers422 validation_errorwith a message startingdmarc_confirmation_required:andplanned_records, every record onboarding would create, so you can review them; the dashboard lists them in the dialog. On an apex, create ap=nonerecord yourself first unless you are sure. The domain's DMARC record shows the policy actually published (yours, or Cloudflare's), read by DNS. - Email preview. Cloudflare keeps the full content of every message (password reset links and one-time codes included) for about 7 days, visible to anyone with access to your Cloudflare dashboard. Stamp0 turns it off unless you tick Keep Cloudflare email preview (
keep_email_preview), and the DNS advisor warns if it is switched back on. - Suppressed recipients are dropped from a send instead of failing the whole request.
- Events first. A domain stays
pendinguntil its event subscription exists, so no delivery event is lost. - One subscription per domain. Cloudflare allows one Email Service event subscription per sending domain. If the domain already has one that Stamp0 did not create (from another tool, or your own Worker), onboarding answers
409 provider_subscription_conflictnaming it, before anything is changed; Stamp0 never takes it over. Delete it in your Cloudflare account or use another domain. If a later onboarding step fails, Stamp0 undoes what it changed (the sending subdomain it created, or the email-preview and suppressed-recipient settings of one that existed).
Onboarding runs with a setup token you pass for that request only (setup_token on POST /domains); removing a Cloudflare domain (DELETE /domains/:id with { "setup_token": … }) deletes its sending subdomain and event subscription, and its Email Routing rules if it receives on Cloudflare, the same way. A domain whose Cloudflare connection was removed (destroyed or disconnected) still has its sending subdomain, and possibly Email Routing, in your account: deleting it answers 409 connection_not_ready unless you pass a setup_token (Stamp0 removes what it set up for the name) or force: true (the domain is deleted and the response's left_behind lists what to remove by hand; the connection shows the same list as a notice). A domain added for receiving only (capabilities.sending: "disabled" and no sending connection) gets no sending identity on any connection. Open and click tracking, a custom tracking domain and enforced TLS are not available on Cloudflare and are refused.
A domain is verified when Cloudflare reports its records ready and the records resolve.
Amazon SES domains on Cloudflare DNS
A domain that sends through Amazon SES but whose DNS is on Cloudflare can have its records published for you through a Cloudflare connection of your organization: Publish on Cloudflare on the domain page, or
POST /domains/:id/dns/cloudflare
{ "connection_id": "con_cloudflare…", "setup_token": "…" }The setup token needs Zone Read and DNS Write on that zone and is used for this request only; the stored runtime token never gets DNS Write. Stamp0 adds the DKIM CNAMEs, the MAIL FROM MX and SPF, and a v=DMARC1; p=none; record only when the domain has no DMARC policy (yours stays). Each record it creates carries the comment managed by stamp0 (<domain id>). It never overwrites: a name that already holds a different value is reported as conflict and left alone, and a receiving MX is always yours to add (manual), since it decides where the name's mail goes. The answer is the domain, whose dns_automation (provider: "cloudflare", applied or partial) lists the result per record; verification then starts again. A domain that sends on Cloudflare needs none of this: Cloudflare publishes its records itself.
Deleting the domain removes the records Stamp0 created, each by its id and only while it still holds what Stamp0 wrote (a record you changed since is yours and stays), so the delete needs a setup token with DNS Write on the zone. Without one it answers 422 setup_token_required, unless you pass force: true, which deletes the domain and lists the records in left_behind. The same goes for the _dmarc p=reject record Cloudflare creates when a domain onboards without a DMARC record.
Receiving
Each domain receives on one connection, chosen independently of the one that sends: in.acme.com can receive on Cloudflare while acme.com sends through SES. A domain that sends on Cloudflare receives on SES or on a Cloudflare connection of the same Cloudflare account: one setup token covers one account, so another account's connection is refused (422 validation_error). Receiving on Cloudflare uses Email Routing in your account and the Stamp0 Worker; nothing passes through Stamp0's servers until Stamp0 reads the stored message from your bucket.
POST /domains
{
"name": "in.acme.com",
"sending_connection_id": "con_ses…",
"receiving_connection_id": "con_cloudflare…",
"receiving": { "enabled": true, "mode": "subdomain_rules" },
"setup_token": "…"
}PATCH /domains/:id turns receiving on ({ "receiving": { "enabled": true }, "setup_token": … }), off ({ "receiving": { "enabled": false }, "setup_token": … }), or moves it to another connection (receiving_connection_id). Moving between two Cloudflare connections is two steps: off, then on with the other one. The response's receiving_connection names the connection and its provider, and capabilities.receiving on the connection says whether it can receive. In the dashboard, Receive through in the add-domain dialog does the same, and the domain's Receiving switch asks for a setup token on Cloudflare. The CLI takes the token on stdin: printf %s "$TOKEN" | stamp0 domains update dom_… --receiving on --setup-token-stdin. The MCP server never takes a setup token, so it refuses to add a domain that sends or receives on Cloudflare and points to the dashboard or the CLI; once the domain exists, the MCP tools follow it.
Turning Email Routing on, safely
Stamp0 turns Email Routing on for the receiving name only (never the whole zone), and guards your other mail:
- It reads your apex
MX, apexTXT(SPF) and_dmarcrecords, checks the setup token can delete records (DNS Write), then turns Routing on for the name. - It reads them again. Cloudflare adds its routing DKIM key (
cf2024-1._domainkey) the first time, and an apexv=spf1 include:_spf.mx.cloudflare.net ~allwhen your apex had no SPF: Stamp0 deletes that SPF record at once (the name keeps receiving without it). Any other change is rolled back: Routing goes off on the name, what it added is deleted, and the API answers422 unsupported_by_providerwithmx_conflict:and the change. - It turns on subaddressing for the zone (so
support+orders@reachessupport@), recording the previous value, which is put back when the connection is removed.
A name on a zone whose apex receives through another provider (Google Workspace, Microsoft 365) works as a subdomain: the apex is untouched.
Modes
receiving.mode | Where | What receives |
|---|---|---|
subdomain_rules (default) | A subdomain (in.acme.com) | Only addresses with an inbox. Each inbox address gets its own Email Routing rule, stamp0:<id>:<inbox id>, to the Stamp0 Worker, added and removed with the inbox (runtime token). Mail to any other address is refused by Cloudflare (550 5.1.1); a new inbox is activating for its first five minutes (see below). Cloudflare allows 200 rules per zone; the 201st inbox answers 422 unsupported_by_provider (rule_limit_reached:) and is not created |
apex_catch_all | The zone apex (acme.com) | Every address. Needs confirm_mx_takeover: true, and is refused (mx_conflict:) when another provider's MX is published on the apex. The zone's catch-all rule is pointed at the Worker; the rule it replaced is recorded and put back when receiving is turned off or the connection is removed |
subdomain_rules on the apex is refused (mx_conflict:), so receiving never silently takes over a domain's mail. The mode is chosen when receiving is turned on: a receiving.mode on a domain that already receives answers 422 validation_error; turn receiving off, then on again with the new mode. A domain whose DNS is not an active zone of this Cloudflare account answers 422 zone_not_on_cloudflare, with a message that starts zone_not_on_cloudflare:.
How a message arrives
- Cloudflare checks SPF, DKIM and DMARC and refuses mail that fails both SPF and DKIM, or an enforcing DMARC policy. It runs the Worker once per recipient.
- The Worker stores the raw message in your R2 bucket with a signed pointer in the object's metadata, then puts the pointer on Stamp0's queue and rings Stamp0's doorbell.
- Stamp0 pulls the queue (on the doorbell, and every minute), verifies the pointer's signature, checks the stored bytes against it, and only then reads the recipients and SPF, DKIM and DMARC verdicts from the pointer: never from headers in the message.
- Copies of one message to several inboxes become one received email (same
Message-IDand the same content after Cloudflare's own trace headers) listing every inbox, with one receipt per copy. A redelivered pointer changes nothing. - Pointers that fail repeatedly reach the dead-letter queue, which Stamp0 drains every five minutes; an hourly sweep reads the signed pointers of stored objects back to the retention horizon and recovers any message whose queue entry was lost. Objects without a valid signature are never processed.
A new inbox needs a few minutes. Cloudflare takes one to three minutes to start accepting mail for a new address. A new Cloudflare inbox shows activating for its first five minutes; mail sent to it meanwhile may be refused with 550 5.1.1 and should be sent again. Stamp0 never suppresses your own inbox addresses because of such a refusal.
Received mail then appears in GET /emails/receiving, the domain's inboxes and threads, and email.received webhooks, as for SES. Downloads (raw.download_url, attachments) are links to the Stamp0 API (GET /emails/receiving/:id/raw, GET /emails/receiving/:id/attachments/:attachmentId/content), which reads the object from your bucket with the runtime token: they need the same API key (or dashboard session) as the read that returned them and expire after an hour.
Status
receiving.status is enabled once Cloudflare reports the name ready. While it is pending, receiving.setup.status says why: setup_token_required (Routing is off for the name, for example turned off in Cloudflare: turn it on again with a setup token), routing_misconfigured (Cloudflare reports a missing or unlocked record), data_plane_missing (the Worker is not deployed). Every verification pass reads Routing's status again, so a name whose Routing was turned off (or a record unlocked) in Cloudflare goes back to pending with the reason; Check DNS now (POST /domains/:id/verify) runs a pass at once, and also puts back the rule of any inbox that lost it.
Turning it off
With a setup token, Stamp0 deletes its own rules for the name, restores the catch-all it replaced, and turns Routing off for the name only if Stamp0 turned it on and no rule of yours remains there. Deleting the domain does the same first.
Sending
-
Throughput. Stamp0 sends through Cloudflare's REST API at 2 emails per second per connection at most (about 170,000 a day). Sends past that are queued, never refused.
-
Daily quota. Cloudflare also has a daily quota that grows with your reputation; new accounts start around 1,000 a day. It belongs to the Cloudflare account, so every connection on the account, and mail you send outside Stamp0, uses the same quota. The connection shows what the account sent in the current window (
sending_quota.sent_last_24_hours) and where the figure comes from (sent_source):cloudflare_analytics: Cloudflare's email analytics, every send on the account (needs Account Analytics Read on the runtime token; the figure is never below Stamp0's own count, since analytics lag a few minutes);stamp0_records: Stamp0's own count across every connection on the account, when the analytics cannot be read.
The window resets when Cloudflare says (
resets_at, which need not be midnight; the next UTC midnight when Cloudflare gives no time). Cloudflare's ownusage.sentcounter is not used: it does not keep up. Sends past the quota wait in the queue asquota_exceededfor the next window, and so does everything after Cloudflare itself reports the quota spent. -
Attachments.
GET /emails/:id/attachmentslinks each attachment toGET /emails/:id/attachments/:attachmentId/contenton the Stamp0 API, which reads it from your bucket with the runtime token and answers it as a download. The link needs the same API key (or dashboard session) as the listing and expires after an hour. -
Message-ID. Cloudflare sets it and it cannot be overridden; Stamp0 stores it and adds
X-Stamp0-Email-Id. Replies keepIn-Reply-ToandReferences. -
Bounces and complaints on any connection block the address for the whole organization, on SES as well. When the recipient's server reports a hard bounce or a complaint on a Cloudflare connection, Stamp0 also adds the address to Cloudflare's suppression list for each of this connection's sending domains (by the domain's own name, never account-wide), retrying for about a day if Cloudflare is busy. Stamp0's list stays the record: removing the address in Stamp0 stops a write that has not happened yet.
-
From must be a domain this connection onboarded.
Transactional only
Cloudflare's documentation says Email Service "supports transactional email only", and its enforcement (bounce, complaint and spam-rejection rates; repeated bulk mail) can disable sending for your whole Cloudflare account, including mail you send outside Stamp0. Stamp0 keeps you on the safe side:
- Campaigns,
/broadcasts, sequences and preference-center broadcasts on a Cloudflare domain answer422 unsupported_by_provider. Campaigns and sequences already using a domain that moves to Cloudflare are paused. - A batch in which more than 50 messages share a subject answers
422 unsupported_by_provider. List-Unsubscribeis accepted (Cloudflare signs it), but a batch send that carries it puts abulk_headers_on_cloudflarenotice on the connection for a week.- One confirmation email per signup (double opt-in) is allowed.
The provider terms say the same.
Limits compared with SES
| Amazon SES | Cloudflare | |
|---|---|---|
| Campaigns, sequences, broadcasts | Yes | No (422) |
| Open and click tracking | Yes | No (422) |
| Message size | 40 MiB | 5 MiB (422) |
| Recipients per message | 50 | 50 |
Custom MAIL FROM, Message-ID, Date, Return-Path | Partly | No (422) |
| Custom headers | Allowed | X-*, List-*, In-Reply-To, References, Precedence, Auto-Submitted, Importance, Priority and similar; up to 20 non-X- headers, 16 KB in all |
Inline images (content_id) | Yes | Yes |
| Send rate | Your SES quota | 2 per second per connection |
| Inbound size | 40 MiB | 25 MiB |
| Receiving addresses | The whole domain | One rule per inbox (200 per zone), or the apex catch-all |
| Region and sandbox | Yes | None |
| DNS elsewhere than the provider | Yes | No (zone_not_on_cloudflare) |
Reputation thresholds
Deliverability judges Cloudflare domains against Cloudflare's own policy: bounces warn at 2% and put the account at risk at 5%, complaints at 0.1% and 0.2%, each from 100 sends. Cloudflare suspends sending after a week at risk. A project that sends through SES and Cloudflare is judged against the stricter of both (health.basis: "strictest").
What it costs
You pay Cloudflare directly, on your own bill (list prices, October 2026):
| Item | Price |
|---|---|
| Workers Paid | $5 a month |
| Sending | 3,000 emails a month included, then $0.35 per 1,000 (each recipient counts) |
| Receiving (Email Routing) | Free |
| R2 storage | 10 GB-month free, then $0.015 per GB-month |
| Queue operations | 1 million free, then $0.40 per million (each message is written, read and acknowledged) |
Plans
Provider connections are counted per provider: Free and Pro include one Amazon SES connection and one Cloudflare connection; Scale has no limit. See pricing.
Connect AWS
Connect an AWS account to Stamp0 with a CloudFormation Quick Create link, verify it with the RoleArn output, and get your SES account out of the sandbox.
Domains
Verify a sending domain in Amazon SES with DKIM, custom MAIL FROM and DMARC, publish the records automatically on Route 53, and set tracking, TLS and receiving per domain.