Migrating from SendGrid, Mailgun, or AWS SES to Tratto
How to switch email providers without losing data, breaking DNS, or causing downtime. A practical, phase-by-phase migration guide—including what Tratto can't migrate for you.
You've outgrown your current email provider. Or you need EU hosting and yours can't give it to you.
Migrating to Tratto is straightforward, with one real exception that we'll cover honestly rather than bury: your suppression list doesn't come with you. Read the "What doesn't migrate" section before you commit to a date.
Phase 1: Setup (No Changes Yet)
First, don't touch your existing provider. Set everything up in parallel.
1. Create Tratto account
Join beta, get your API key.
2. Configure your domain
In the Tratto dashboard:
- Add your custom domain
- Copy the three DKIM CNAME records into your DNS
- Add the SPF include (
v=spf1 include:amazonses.com ~all), merging it into your existing SPF record if you have one—never publish two - Don't remove your old provider's records yet
Verification is a background poll that re-checks pending domains every 10 minutes; POST /v1/domains/{id}/verify checks immediately if you don't want to wait. DNS propagation itself can take hours. See the domain setup docs for the details.
3. Test with a staging domain
Don't use your production domain yet. Use staging.yourdomain.com:
const tratto = new Tratto(process.env.TRATTO_API_KEY!)
await tratto.emails.send({
from: '[email protected]',
to: '[email protected]',
subject: 'Test from Tratto',
html: '<p>If you see this, it worked!</p>',
})Check that emails arrive. Good? Move to phase 2.
Phase 2: Parallel Running (Both Providers Active)
Run both providers simultaneously for a week. This catches issues before they hit production.
Update your code to use Tratto for new emails:
// Before
import sgMail from '@sendgrid/mail'
sgMail.setApiKey(process.env.SENDGRID_API_KEY!)
// After
import { Tratto } from '@tratto/email'
const tratto = new Tratto(process.env.TRATTO_API_KEY!)
export async function sendEmail(to: string, subject: string, html: string) {
try {
// Try Tratto first
await tratto.emails.send({ from: '[email protected]', to, subject, html })
} catch (err) {
// Fallback to SendGrid while testing
console.error('Tratto failed, falling back to SendGrid:', err)
await sgMail.send({ from: '[email protected]', to, subject, html })
}
}Monitor both providers:
// Track which provider sent each email
await db.emailLog.create({
to,
subject,
provider: 'tratto', // or 'sendgrid'
status: 'sent',
timestamp: new Date(),
})Watch for issues:
- Check delivery rates in both Tratto and SendGrid dashboards
- Monitor bounce rates
- Check spam complaints
- Review any errors in logs
Run for 7-14 days. If everything looks good, proceed to phase 3.
Phase 3: Cutover
Once you're confident, switch fully:
export async function sendEmail(to: string, subject: string, html: string) {
// No more fallback
await tratto.emails.send({
from: '[email protected]',
to,
subject,
html,
})
}Update DNS:
- Remove the old provider's
includefrom your SPF record - Keep
include:amazonses.com - Leave the three DKIM CNAMEs alone—deleting them un-verifies the domain
Set up webhooks:
Webhook event types are bare strings—sent, delivered, opened, clicked, bounced, complained, unsubscribed—not namespaced:
// Handle deliveries, bounces, opens
export async function handleTrattoWebhook(event: any) {
if (event.type === 'delivered') {
await updateDeliveryStatus(event.emailId, 'delivered')
}
if (event.type === 'bounced') {
// `emailId` and `recipient` are top-level, not under `data`. For a bounce,
// `data` is { type, subType } — `type` is 'Permanent' or 'Transient'.
await handleBounce(event.recipient, event.data.type)
}
}Note that webhooks aren't available on the Free plan—they start at Starter. If you're evaluating on Free, that's the one integration you can't test.
What doesn't migrate
This is the part that decides your timeline, so don't skim it.
Your suppression list doesn't transfer, and there is no API to import it. Tratto has no suppression endpoint, and the SDK has no suppression resource. Contacts that reach bounced status are also locked there deliberately: the API rejects attempts to move them to another status, precisely so a human can't undo a machine's deliverability decision from a dropdown.
The practical consequence: on day one, Tratto does not know that an address hard-bounced at SendGrid three years ago. If you send to your full list, you will re-send to addresses your old provider had already written off, and those bounces land on your new sending reputation.
Plan around it:
- Export your bounce and complaint lists anyway, before you cancel anything. You'll want them even if you can only use them on your side.
- Filter at your application layer. Keep the exported suppression list in your own database and check against it before calling
emails.send(). This is the only reliable option today. - Don't blast the full list at cutover. Warm up: start at a few hundred a day and climb. This is good practice regardless, and here it's doing double duty by surfacing dead addresses slowly instead of all at once.
We'd rather tell you this now than have you discover it from a bounce rate.
Templates don't transfer either. Tratto templates are HTML or Markdown; you'll be rewriting whatever your old provider's editor produced.
Phase 4: Decommission Old Provider
Once Tratto is handling 100% of email for 2+ weeks:
- Export historical data—delivery logs, bounce lists, complaint lists. Do this before cancelling; access usually dies with the subscription.
- Delete API keys—revoke all SendGrid/Mailgun/AWS credentials
- Remove DNS records—clean up the old provider's SPF include and DKIM records
- Cancel subscription
Migration Checklist
- Bounce list exported and loaded into your database as a pre-send filter
- Complaint list exported and handled the same way
- Templates rewritten as HTML or Markdown
- Webhooks configured (Starter or above)
- Team access—invite team members to the Tratto dashboard
- Warm-up schedule agreed, rather than a full-list cutover
Common Migration Issues
"My emails are landing in spam now"
Likely causes:
- DNS not fully propagated (wait; SES looks for records for up to 72 hours)
- Old provider's DNS records conflicting—in particular a second SPF record
- New sending pattern on a new path; early volume gets scrutinised
Solution: warm up by sending small volumes (100/day) for a few days.
"Webhooks aren't firing"
Check:
- You're not on the Free plan (webhooks start at Starter)
- Webhook URL is correct and publicly accessible
- Your endpoint returns 200 within 10 seconds—that's the delivery timeout, and a slower reply is recorded as a failed attempt and retried
- You're matching bare event types (
delivered, notemail.delivered)
Cost Comparison
Public list prices, checked 16 September 2026. Check them yourself before deciding—they move.
| Provider | Model | Cost |
|---|---|---|
| SendGrid | Tiered plans | Essentials from $19.95/mo; Pro from $89.95/mo (includes up to 1,000 teammates) |
| Mailgun | Tiered plans | ~$35/mo for 50k emails (Foundation) |
| AWS SES | Pure usage | ~$10 for 100k emails ($0.10/1,000, plus $0.12/GB of attachments) |
| Tratto | Volume-based flat rate | €29/mo for 75k emails (Starter), VAT excluded |
Read that table honestly: AWS SES is cheaper than Tratto and will stay cheaper. It's a raw sending API—you bring your own dashboard, contact management, campaigns, suppression handling and analytics. If your team is happy doing that, SES is the rational choice on cost alone and we're not going to pretend otherwise.
Against SendGrid and Mailgun, Tratto is in the same broad range rather than dramatically below it. Pick on data residency, on scope, or on whether you like the product—not on the assumption that switching saves you a large percentage.
Timeline
- Day 1-2: Setup and DNS
- Day 3-7: Parallel running (both providers)
- Day 8-14: Monitoring, old provider dropping toward 0%
- Day 15+: Cleanup and decommissioning
Questions about migration? Join the beta—our team can walk you through it.