Migrate from SendGrid

Moving from SendGrid means verifying your domain in Plunk, importing contacts together with every suppression list, converting Handlebars templates to Liquid, and replacing mail/send calls. SendGrid keeps sending until you switch.

Updated · 9 min read

What moves, and what you rebuild

From SendGridIn PlunkHow
Domain authenticationDomainNew DNS records, alongside SendGrid’s
Marketing contactsContactsCSV import
Suppressions (all six types)Unsubscribed contactsCSV import with subscribed set to false
Dynamic templatesTemplatesCopy the HTML, convert Handlebars to Liquid
mail/send callsPOST /v1/sendRestructure the request
AutomationsWorkflowsRebuild by hand; SendGrid has no export
Event WebhookWorkflows with a webhook stepRebuild by hand

1. Verify your domain in Plunk

Add your sending domain under Settings → Domains in Plunk. Plunk shows three CNAME records named <token>._domainkey, pointing to <token>.dkim.amazonses.com. Add them at your DNS provider and Plunk marks the domain verified once they resolve.

You do not need to remove anything first. SendGrid’s domain authentication uses CNAMEs on em#### and the s1._domainkey and s2._domainkey selectors. Plunk’s DKIM selectors are different, so both sets of records can live side by side and SendGrid keeps working while you move.

Plunk also offers an optional custom MAIL FROM domain: an MX and an SPF TXT record on a plunk. subdomain. It aligns bounces with your domain and is worth adding, but sending works without it.

2. Move contacts and suppressions

In SendGrid, open Marketing → Contacts, use the action menu on a list and choose Export. SendGrid emails you when it is ready, and the download link lasts 12 hours. The API equivalent is POST /v3/marketing/contacts/exports. Rename the address column to email if it is called something else.

SendGrid keeps opt-outs and failures out of the contact list, in six suppression lists. Bring all of them, each as rows with subscribed set to false:

Suppression listWhere to export it
Global unsubscribesSuppressions → Global Unsubscribes → Download as CSV, or GET /v3/suppression/unsubscribes
Group unsubscribesGET /v3/asm/suppressions (see the questions below)
BouncesSuppressions → Bounces → Export CSV, or GET /v3/suppression/bounces
Spam reportsGET /v3/suppression/spam_reports
Blocks and invalid emailsListed under Suppressions in the dashboard
ColumnWhat Plunk does with it
emailRequired. Rows without a valid address are skipped and listed as errors.
subscribedOptional. true, 1 or yes subscribes the contact; any other value unsubscribes it. Empty keeps an existing contact as it is; new contacts default to subscribed.
Anything elseStored as a custom field. Headers are lowercased, so firstName becomes firstname and First Name becomes first name.

3. Convert templates

Fetch each dynamic template’s active version with GET /v3/templates/{template_id}/versions/{version_id}; the html_content field is what you paste into a Plunk template in HTML mode. Plunk renders with Liquid, which reads like Handlebars for plain variables but differs for logic:

Handlebars (SendGrid)Liquid (Plunk)
{{first_name}}{{first_name}}
{{{html_snippet}}}{{html_snippet}}
{{#if premium}} … {{else}} … {{/if}}{% if premium %} … {% else %} … {% endif %}
{{#each items}} {{this.name}} {{/each}}{% for item in items %} {{item.name}} {% endfor %}
{{formatDate date "MMM D, YYYY"}}{{date | date: "%b %-d, %Y"}}

4. Change your sending code

SendGrid nests recipients and their data in personalizations. Plunk keeps it flat: one request can list several recipients, and each gets its own email with the same data.

SendGrid fieldPlunk fieldNotes
personalizations[].totoA string, {name, email} or an array
personalizations[].dynamic_template_datadataShared by every recipient in the request
template_idtemplateThe Plunk template’s ID
from {email, name}from {email, name}
reply_to {email}replyAn address string
subjectsubject
content[{type: "text/html"}].valuebodyPlunk builds the plain-text part itself
attachments[] {content, filename, type, content_id}attachments[] {content, filename, contentType, contentId}
headersheaders
cc, bcc, send_at, categories, custom_args, asm, ip_pool_name(none)Not supported on the send endpoint
tracking_settings(project setting)Track all email, marketing only, or nothing
Before: SendGrid
await fetch('https://api.sendgrid.com/v3/mail/send', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    Authorization: `Bearer ${process.env.SENDGRID_API_KEY}`,
  },
  body: JSON.stringify({
    personalizations: [
      {to: [{email: 'ana@acme.com'}], dynamic_template_data: {first_name: 'Ana'}},
    ],
    from: {email: 'hello@acme.com', name: 'Acme'},
    template_id: 'd-1234567890abcdef',
  }),
});
After: Plunk
const response = await fetch('https://send-api.akmarketingseo.com/v1/send', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    Authorization: `Bearer ${process.env.PLUNK_SECRET_KEY}`,
  },
  body: JSON.stringify({
    to: 'ana@acme.com',
    from: {email: 'hello@acme.com', name: 'Acme'},
    template: 'YOUR_PLUNK_TEMPLATE_ID',
    data: {first_name: 'Ana'},
  }),
});

if (!response.ok) {
  throw new Error(`Plunk returned ${response.status}`);
}

If your app sends through SendGrid’s SMTP server instead of the API, only the settings change:

SMTP settings
# Before (SendGrid)
host:     smtp.sendgrid.net
username: apikey
password: your SendGrid API key

# After (Plunk)
host:     smtp.useplunk.com
port:     587 (STARTTLS) or 465 (TLS)
username: plunk
password: your Plunk secret key (sk_...)

5. Rebuild automations

SendGrid has no way to export an automation, so list each one’s trigger, emails and delays from the dashboard and rebuild it as a Plunk workflow. A workflow starts from a tracked event, a segment entry, a schedule or a manual run, and can send, wait, branch on conditions, call a webhook and update the contact.

SendGrid automations start when a contact joins a list. The closest Plunk trigger is a segment entry: build a segment on the field that put the contact on the list and start the workflow when they enter it.

6. Rebuild the Event Webhook

Plunk has no global webhook setting. Instead, you create a workflow that starts on an event and add a Webhook step that posts to your URL. One workflow per event you care about, each with its own URL and headers if you want them.

SendGrid eventPlunk event
processedemail.sent
deliveredemail.delivery
openemail.open
clickemail.click
bounceemail.bounce
spamreportemail.complaint
unsubscribe, group_unsubscribecontact.unsubscribed
dropped, deferred(no equivalent)

Plunk’s email.open and email.click fire on the first open and first click of each email, not on every one. The payloads differ, so the code that reads them needs updating. Plunk’s payload for each event is listed in the webhooks guide.

7. Switch over

  1. Send to yourself first

    Send a few real templates from Plunk to your own inboxes and check rendering, links and variables. Gmail’s “Show original” should report DKIM pass for your domain.
  2. Move one kind of email at a time

    Switch one flow, such as password resets, and watch delivery in Plunk for a day before moving the rest. SendGrid stays available as a fallback the whole time.
  3. Remove the old records last

    Once nothing sends through SendGrid any more, wait a few days for any retries to settle, then remove its DNS records and cancel the plan.

Keep reading

Frequently asked questions

Move from SendGrid at your own pace.

Start on the free plan with 1,000 emails a month, no credit card needed. When you need more, every email is $0.001, with unlimited contacts and every feature included.