Skip to content

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

  1. 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.
  2. 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).
  3. 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 set SMTP_HOST as 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 EMAIL binding remains the secretless same-account path;
  • the HTTPS send_raw API 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 a Return-Path on your own domain (not cf-bounce.).
  • API check: GET /api/send/emails/{id} (or emails.get in 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.