Route sending lanes to your own provider
Point the transactional, lists, or outreach lane at Amazon SES or an SMTP relay — profile fields, where credentials go, how failures surface, and how to verify with doctor.
Tinbox sends through Cloudflare Email Sending by default — zero-config on your
own Cloudflare account. That default has one consequence worth knowing:
Cloudflare's relay stamps Feedback-ID (an RFC 6449 bulk-sender complaint
identifier) and routes bounces via cf-bounce.<domain> on every message,
including 1:1 mailbox replies. Authentication stays clean (SPF/DKIM/DMARC all
pass), but the transport shape reads as campaign mail to inbox classifiers.
The fix is lane routing (Settings → Providers): each of the three lanes —
transactional (replies, receipts, auth, composer/API sends), lists
(newsletters), outreach (cold) — can point at its own provider profile.
Routing the transactional lane to SES or an SMTP relay bypasses the Cloudflare
relay entirely: no Feedback-ID, no cf-bounce, your MIME Message-ID stays on
the wire.
Amazon SES
- In AWS: verify your sending domain in SES, leave the sandbox, and create an
IAM user with
ses:SendRawEmail. Note the region, access key id, and secret. - In Tinbox, Settings → Providers → Add provider:
- Provider: Amazon SES
- Label: e.g.
SES — production - Region: e.g.
us-east-1 - Access key id / secret access key: paste them — they're sealed with the instance master key and stored in D1 (never in config files).
- In the Routing table, set Transactional → your SES profile. Save.
Prefer worker-level secrets over in-app storage? Set them on the worker instead and leave the profile's credential fields empty — env always wins:
wrangler secret put SES_ACCESS_KEY_ID
wrangler secret put SES_SECRET_ACCESS_KEY
# optional: wrangler secret put SES_SESSION_TOKEN
(Region can also come from a SES_REGION var in wrangler.jsonc.)
Generic SMTP relay (Postmark, Mailgun, self-hosted, …)
Same flow, provider SMTP relay:
- Host: e.g.
smtp.postmarkapp.com(or setSMTP_HOSTas a worker var) - Port / TLS: defaults by security posture (587 STARTTLS / 465 TLS / 25)
- Username + password: in the profile (sealed into D1), or as worker secrets:
wrangler secret put SMTP_USERNAME
wrangler secret put SMTP_PASSWORD
Then route Transactional → your SMTP profile.
Cloudflare's authenticated SMTP is a different boundary
Cloudflare Email Service now also accepts authenticated SMTP submission from
external applications at smtp.mx.cloudflare.net:465 using implicit TLS and an
Email Sending: Edit API token. That does not make it a useful SMTP profile for a
Tinbox Worker:
- Workers expose outbound TCP, but cannot connect to Cloudflare-owned IP ranges, so they cannot reach Cloudflare's own SMTP host;
- the native
EMAILbinding remains the secretless same-account path; - the HTTPS
send_rawAPI is the appropriate customer-account/token path from a Worker; - an external application that submits directly to Cloudflare SMTP bypasses Tinbox's authorization, suppression, idempotency ledger, Sent persistence, and delivery timeline.
If a legacy application must speak SMTP and retain Tinbox policy/audit, place the default-off Tinbox SMTP compatibility gateway on a TCP-capable VPS/container and have it call the authorized Tinbox HTTPS API. A Worker cannot expose an inbound SMTP listener. Cloudflare Email Service is transactional-only, regardless of whether the message enters through the binding, REST, or SMTP.
Route the whole instance instead (source config)
If you'd rather not use per-lane routing at all, the legacy single-provider
switch moves every unrouted lane at once: set mail in the worker config
(createWorker({ mail: { provider: "ses", region: "us-east-1" } })) or the
MAIL_PROVIDER env var, with the same SES_*/SMTP_* secrets. Per-lane
routing, when present, wins over this for the lanes it routes.
What happens when credentials are missing
Nothing silent. A lane routed to a profile whose required fields are absent
(SES without keys, SMTP without a host, a deleted profile id) resolves to a
failing dispatcher: every send on that lane fails immediately with a clear
reason in the send ledger (Settings → Activity, or emails.get via the API),
and becomes retryable the moment the credential is set. Tinbox never falls back
to a different provider than the one you routed — sending 1:1 mail through the
wrong relay is worse than a loud failure. An unrouted lane keeps the
default Cloudflare path, unchanged.
Verify
tinbox doctor— the Send providers (lane routing) section shows each lane's resolved profile and whether its credentials are present (sealed in D1 / worker env / missing), before any send is attempted.- Send a real test: Settings → Diagnostics → Send test email, then check
the received headers — no
Feedback-ID, and aReturn-Pathon your own domain (notcf-bounce.). - API check:
GET /api/send/emails/{id}(oremails.getin the SDK) shows the per-recipient ledger status and the event timeline for any send.
Notes
- Cloudflare Email Sending is transactional-only under Cloudflare's AUP; SES
and SMTP profiles can serve any lane. The
relay(HTTP cold-email) provider is for the outreach/lists lanes — see Set up cold-outreach SMTP. - Cloudflare Email Routing is not an IMAP/POP3 mailbox. It forwards incoming mail
or invokes Tinbox's
email()handler; Tinbox then stores and exposes the message through its own API, SDK, CLI, and web inbox. - Keep cold outreach on its own provider and subdomain regardless of what carries your transactional mail — reputation isolation cuts both ways.