JustEmails
PricingSign inStart free trialStart free
Tutorials··15 min read

Migrate from SendGrid to JustEmails API: A Transactional Cutover Plan

How to migrate from SendGrid without tanking deliverability — and the one DNS record that breaks every old link if you delete it on cutover day.

By JustEmails Platform Team
Contents
  1. What you'll have when this is done
  2. Before you start
  3. Step 1: Inventory what SendGrid is actually doing for you
  4. Step 2: Add JustEmails DNS alongside SendGrid's
  5. Step 3: Export the suppression list — all five of them
  6. Step 4: Ship a dual-send wrapper — then break it on purpose
  7. Step 5: Rewrite the webhook handler for both shapes
  8. Step 6: The watch window
  9. Step 7: Cut over — but don't delete that one record
  10. Common errors when you migrate from SendGrid
  11. What SendGrid still does that we don't
  12. Next steps
  13. Frequently Asked Questions
  14. Will I lose emails when I migrate from SendGrid?
  15. How do I move my SendGrid suppression list to JustEmails?
  16. Do I need new DNS records to migrate from SendGrid?
  17. What SendGrid features don't have a JustEmails equivalent?
  18. Try JustEmails

The export button was what stopped me.

I'd decided on a Tuesday night to migrate from SendGrid — just a side project's transactional email, nothing dramatic — felt fine about it, wrote the new client in about twenty minutes, and then went looking for the suppression list. Three years of hard bounces and spam complaints. It lived in exactly one place: their dashboard. Not in my database. Not in a backup. Theirs.

That's the moment the job stops being a code change and turns into a data problem. And it's the reason most SendGrid migration guides are useless: they show you how to swap an HTTP client and say nothing about the pile of accumulated reputation state you're about to walk away from.

So here's the plan I'd run again. It assumes you want to consolidate — mailbox hosting and transactional sending under one provider, one DNS setup per domain, one bill. If that's not what you're after, skip it. Swapping providers because a dashboard annoyed you is a bad reason to spend a week on DNS.

What you'll have when this is done

Transactional email flowing through the JustEmails API, your domain signing with the right DKIM keys, your suppression list in your own database where it belongs, webhooks feeding your bounce handling, and a SendGrid account you can cancel without holding your breath.

Hands-on time is maybe three hours. Calendar time should be a week, because the watching is most of the work — and the watching is the part I've cut short twice now, both times out of impatience, both times regretted.

Before you start

  • A JustEmails account with your sending domain added. If you're already hosting mailboxes there, it's done — you just need an API key.
  • DNS access. Cloudflare, Route 53, Namecheap, whatever you're on.
  • SendGrid admin access with a full-access API key (read-only won't get you the suppression endpoints).
  • Your current sending volume. Go to SendGrid's stats page and write down a monthly number and a peak-day number.

That last one matters more than it sounds. JustEmails includes 1,000 API emails a month in the $49/year plan, and additional capacity is $25/year per 10,000-emails/month tier. Whether that beats what SendGrid is charging you depends entirely on what SendGrid is charging you, and I'm not going to print a competitor's price here that'll be stale by the time you read this. Go pull your last invoice.

The peak-day number is the one to check first, because the daily ceiling binds before the monthly allowance does. Sends are capped per account per day — 20/day for the first week, 200/day in the second, 500/day from day 15 on — with mailbox sends and API sends counted against the same number, which puts the practical maximum at about 15,000 a month; a tier raises the monthly allowance, not that daily cap. Warm-up runs from domain verification rather than signup, so adding this sending domain to a long-standing account starts that domain back at the bottom of that ramp.

API access requires a paid plan. This cutover runs on the transactional API, and API access is not included in the 7-day trial — a trial account cannot issue the API key the first bullet above asks for, so there is no way to run it without subscribing first.

Step 1: Inventory what SendGrid is actually doing for you

Before you touch anything, write down every job SendGrid is currently doing. Not "sending email." The specific jobs.

Sending. Which services, which endpoints, which from-addresses.

Dynamic templates. SendGrid stores these server-side and you reference them by ID. JustEmails doesn't — you render in your app and post the finished HTML. Export every template body now, before you lose access. There's no bulk export, so it's open-copy-paste-repeat, and it's exactly as tedious as that sounds.

Webhooks. Every URL you've pointed the Event Webhook at, and every event type you subscribed to.

Click tracking. If it's on, your links are being rewritten through a tracking hostname on your domain. Remember this. It comes back to bite people in step 7.

Marketing. Contact lists, campaigns, signup forms. If any of this is live, you're not migrating it — see the honest section near the end.

Fifteen minutes in a text file, and it saved me two "oh no, what about—" moments later.

Step 2: Add JustEmails DNS alongside SendGrid's

Don't replace. Add.

DKIM keys are per-provider — there's no such thing as porting them — so JustEmails signs with its own selectors while SendGrid's stay live:

je1._domainkey.yourdomain.com  CNAME  je1.dkim.justemails.app
je2._domainkey.yourdomain.com  CNAME  je2.dkim.justemails.app

SendGrid's automated security records (s1._domainkey, s2._domainkey, the em#### return-path CNAME) stay exactly where they are. Both providers signing the same domain is normal and correct during an overlap.

SPF gets both senders — SendGrid's include, and JustEmails' a: mechanism:

v=spf1 include:sendgrid.net a:mail1.justemails.app ~all

JustEmails doesn't publish an include. spf.justemails.app and _spf.justemails.app have no TXT record at all, so an include: pointed at either authorises nothing — SPF fails while the record reads as correct in your DNS panel. Use a:mail1.justemails.app, which authorises the IP that host resolves to and costs you one DNS lookup rather than an include's nested chain.

This is where migrations break. SPF allows ten DNS lookups per evaluation, and every include: burns at least one — often several, because includes nest. Add a second sender to a record that already has your mailbox host, your CRM, and your help desk in it and you can blow the limit, which returns PermError, which means every check fails, which means both providers start landing in spam simultaneously. Check your lookup count before you publish, not after. We wrote up the SPF PermError and flattening fix after hitting exactly this.

DMARC doesn't change. Same record, same policy. But if you're still on p=none this is a decent moment to plan the ramp — our DMARC enforcement walkthrough covers going to reject without blackholing your own mail.

Give it an hour to propagate. Verify in the JustEmails dashboard before writing any code.

Step 3: Export the suppression list — all five of them

Here's the part people get wrong: SendGrid doesn't have a suppression list. It has five, and they mean different things.

BASE="https://api.sendgrid.com/v3"
AUTH="Authorization: Bearer $SENDGRID_API_KEY"

for list in suppression/bounces suppression/blocks \
            suppression/invalid_emails suppression/spam_reports; do
  curl -sG "$BASE/$list" -H "$AUTH" \
    --data-urlencode "limit=500" --data-urlencode "offset=0" \
    > "sendgrid-$(basename $list).json"
done

# global unsubscribes live somewhere else entirely
curl -sG "$BASE/asm/suppressions/global" -H "$AUTH" \
  --data-urlencode "limit=500" --data-urlencode "offset=0" \
  > sendgrid-global-unsubscribes.json

Paginate properly. Bump the offset until a page comes back empty — grabbing one page and calling it done is how you end up with a 500-row list that should have been 4,000.

Now the opinion, and it's a strong one: don't look for a way to import this into your new provider. Load it into your own database.

CREATE TABLE suppressed_addresses (
  email       TEXT PRIMARY KEY,
  reason      TEXT NOT NULL,        -- bounce | block | invalid | complaint | unsubscribe
  source      TEXT NOT NULL,        -- 'sendgrid-import' | 'justemails-webhook'
  suppressed_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

Check it before every send. Yes, that's an extra query on the send path. It's also the difference between a migration and a rebuild — the next time you change providers, and there will be a next time, your hard-earned list of dead addresses is a config change away instead of a support ticket.

Vendors love owning this data. Don't let them.

Step 4: Ship a dual-send wrapper — then break it on purpose

Try JustEmails first, fall back to SendGrid on any failure:

// mailer.js
export async function sendTransactional({ to, subject, html, text, key }) {
  if (await isSuppressed(to)) return { skipped: 'suppressed' };

  try {
    const res = await fetch('https://justemails.app/api/v1/send', {
      method: 'POST',
      headers: {
        Authorization: `Bearer ${process.env.JUSTEMAILS_API_KEY}`,
        'Content-Type': 'application/json',
        'Idempotency-Key': key,   // same key = no double-send on retry
      },
      body: JSON.stringify({
        from: { email: 'noreply@yourdomain.com', name: 'YourApp' },
        to: [{ email: to }],
        subject, html, text,
      }),
    });
    if (res.ok) return { provider: 'justemails' };
    throw new Error(`justemails ${res.status}`);
  } catch (err) {
    metrics.increment('mail.fallback', { reason: err.message });
    return sendViaSendGrid({ to, subject, html, text });
  }
}

Use the idempotency key. JustEmails supports them on the send endpoint, and the failure mode they prevent is nasty: your request times out at the network layer after the message was accepted, your retry logic fires, and the customer gets two password reset emails with two different valid tokens.

Now the part everyone skips. Point JUSTEMAILS_API_KEY at garbage in staging and send something. Did it fall back? Did the metric fire? Did anything in your logs actually tell you it happened? A fallback path you've never exercised isn't a safety net, it's a decoration.

I've shipped one that silently swallowed the error and returned success. Found out four days later.

Step 5: Rewrite the webhook handler for both shapes

SendGrid posts an array of events with an event field. JustEmails posts a single event with event_type. Different shapes, same facts. During the overlap you'll be getting both, so branch on the shape:

export async function POST(request) {
  const body = await request.json();

  // SendGrid: array of events, each with `event`
  if (Array.isArray(body)) {
    for (const e of body) {
      if (['bounce', 'dropped', 'spamreport'].includes(e.event)) {
        await suppress(e.email, e.event);
      }
    }
    return new Response('ok');
  }

  // JustEmails: single event, `event_type`
  switch (body.event_type) {
    case 'bounced':
      await suppress(body.recipient, 'bounce'); break;
    case 'complained':
      await suppress(body.recipient, 'complaint'); break;
    case 'delivered':
      await recordDelivery(body.message_id); break;
  }
  return new Response('ok');
}

Every bounce and complaint writes straight into the same suppressed_addresses table from step 3. That's the whole point — one list, two sources, and it survives the provider change. There's more on why complaint handling drives sender reputation in our bounce and complaint webhook guide.

Register the webhook URL in JustEmails while SendGrid's is still firing. Both, for now.

Step 6: The watch window

Deploy. Then leave it alone for a week.

Three numbers matter: the fallback counter (should be zero), the bounce rate on the JustEmails side (a spike means your suppression check isn't running — not that the new provider is worse), and delivery latency, which you want to compare against your own SendGrid baseline rather than anybody's published benchmark, because your traffic mix isn't their traffic mix.

A week, not 72 hours, and it needs to include a weekend. Weekday traffic won't show you the batch job that runs Sunday at 3am and sends 400 receipts in ninety seconds. Ask me how I know.

If you want the new sending path visible next to everything else that can break, a status page built from real uptime data beats squinting at two dashboards.

Step 7: Cut over — but don't delete that one record

Fallback counter's been at zero for a week. Time to finish.

  1. Remove the SendGrid branch from mailer.js.
  2. Trim SPF back: v=spf1 a:mail1.justemails.app ~all (plus your other legitimate senders).
  3. Leave SendGrid's DKIM records alone for another 48 hours. DKIM is verified on receipt, not on send, so anything still sitting in their queue needs those keys to validate when it lands.
  4. Disable the SendGrid Event Webhook.
  5. Keep the account open, unbilled if you can, for 30 days.

And do not delete the click-tracking CNAME.

If SendGrid's click tracking was on, every link in every email you've ever sent points at a hostname on your domain that resolves to their infrastructure. Someone opens a two-year-old receipt tomorrow and clicks. Kill that record on cutover day and the link 404s — the email looks broken, and it's the kind of breakage nobody reports, they just quietly stop trusting your mail.

Leave it up until the old links stop getting traffic, which for receipts and invoices means months. Worth monitoring that hostname's health alongside your landing pages so you find out before a customer does.

Common errors when you migrate from SendGrid

401 Unauthorized from the JustEmails API. Nine times out of ten it's a trailing newline in the environment variable. It's always whitespace.

SPF PermError right after step 2. Too many DNS lookups. Flatten or trim.

429 Too Many Requests. Either the monthly allowance (base is 1,000 emails/month; add tiers at $25/year per 10,000/month) or the daily send cap, which a tier does not lift. Respect the retry_after header rather than hammering. (I hammered. It does not help.)

Bounce rate triples on day one. Your suppression check isn't wired in, and you're mailing dead addresses on a fresh sending path. Stop sending, fix the check, then resume. Bad first impressions on a new setup are expensive to undo.

Templates render with empty variables. SendGrid's dynamic template syntax isn't identical to Handlebars or Liquid — conditionals and array iteration differ. Test every template by hand. All of them. It's tedious and there's no shortcut.

What SendGrid still does that we don't

Being straight with you: this migration isn't a strict upgrade.

SendGrid has Marketing Campaigns, contact lists, signup forms, an Email Validation API, dedicated IPs with warmup tooling, and server-side templates your non-technical teammates can edit without a deploy. JustEmails has none of that. It's transactional sending bundled into email hosting — that's the shape of the product.

So if you're running real marketing volume through SendGrid, migrate the transactional traffic and leave the marketing side alone. Two providers doing two different jobs is a fine end state. I'd go further: it's usually the correct one, and the urge to get everything onto a single invoice has broken more stacks than it's fixed. The consolidation win here is narrow and specific: if SendGrid was only ever sending your password resets and receipts while you paid someone else for mailboxes, the $49/year that covers your transactional sending is also covering unlimited domains and unlimited mailboxes.

Next steps

Watch Google Postmaster Tools for the first month. A sending-path change is exactly when reputation shifts show up, and Postmaster is the one place you'll see Gmail's opinion of you before Gmail acts on it. Read your DMARC aggregate reports too — boring XML, genuinely useful. Your mileage may vary on how fast the new source starts aligning; mine took about nine days. And if you're still deciding between the REST API and plain SMTP relay for parts of your stack, our SMTP versus API breakdown covers where each one wins.

Frequently Asked Questions

Will I lose emails when I migrate from SendGrid?

Not if you overlap the two providers instead of flipping between them. Add the JustEmails DNS records alongside SendGrid's, ship a wrapper that tries JustEmails first and falls back to SendGrid on failure, then watch it for a full week including a weekend. Only when the fallback counter has sat at zero across your real traffic pattern do you remove SendGrid from the code. The dangerous migrations are the ones done at 2am with a single environment variable flip.

How do I move my SendGrid suppression list to JustEmails?

Export it from SendGrid's v3 API and store it in your own database rather than handing it to another vendor. There are five separate lists, not one — bounces, blocks, invalid_emails, spam_reports, and global unsubscribes under /v3/asm/suppressions/global. Pull all five with pagination, load them into a suppressed_addresses table, and check that table before every send. Owning the list yourself means the next migration is a config change, not a data-recovery project.

Do I need new DNS records to migrate from SendGrid?

Yes. DKIM keys are per-provider, so JustEmails signs with its own selectors and you'll add those CNAMEs while SendGrid's stay in place. Your SPF record has to carry both senders during the overlap — SendGrid's include plus JustEmails' a:mail1.justemails.app mechanism, since we don't publish an include — and that extra lookup is where people hit the 10-lookup PermError limit. DMARC doesn't change at all — same policy, same record — but watch your aggregate reports through the transition to confirm the new source is aligning.

What SendGrid features don't have a JustEmails equivalent?

Marketing Campaigns, contact lists and signup forms, the Email Validation API, dedicated IPs with warmup tooling, and server-side dynamic templates. JustEmails is transactional sending bundled with mailbox hosting — 1,000 API emails a month on the $49/year plan, $25/year per additional 10,000-emails/month tier, inside a 500-a-day send ceiling. If you're running real bulk marketing through SendGrid, migrate the transactional traffic and leave the marketing side where it is.


Try JustEmails

Unlimited custom domain email hosting for $49/year flat — unlimited domains, unlimited mailboxes, 10 GB storage, full IMAP/SMTP. Built for agencies, freelancers, and anyone managing email across more than one domain.

Start your 7-day free trial → · How it compares

migrate-from-sendgridsendgrid-alternativetransactional-email-apiemail-api-migrationsuppression-list-managementbuildinpublicsaasstudioaiworkforcebuildwithclaude

Related posts

Guides
Custom Domain Email: What It Costs and How to Actually Set One Up
13 min read
Guides
Email Domain Price: What a Custom Domain Email Address Really Costs
13 min read
Tutorials
Listmonk SMTP Setup (and Mautic's Mailer DSN) with JustEmails
18 min read