Migrate from Resend

Moving from Resend means verifying your domain in Plunk, importing contacts with their subscription status, porting templates, and changing your send calls. Resend can keep sending the whole time.

Updated · 8 min read

What moves, and what you rebuild

From ResendIn PlunkHow
DomainDomainNew DNS records, alongside Resend’s
ContactsContactsCSV import
Unsubscribes and suppressionsUnsubscribed contactsCSV import with subscribed set to false
TemplatesTemplatesCopy the HTML, rewrite the variables
emails.send callsPOST /v1/sendRename a few fields
Automations and eventsWorkflows and tracked eventsRebuild by hand
WebhooksWorkflows 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. Resend signs with a TXT record at resend._domainkey and puts its SPF and MX records on the send. subdomain. Plunk’s DKIM selectors are different, so both sets of records can live side by side and Resend 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

Export contacts from the Contacts page in Resend with Export. Exports over 1,000 contacts arrive by email. Resend does not document the export’s columns, so open the file and check that every contact you have unsubscribed is marked as such.

If it is not, use the API instead: GET /contacts returns each contact’s email, first_name, last_name and unsubscribed. Note the flag is inverted: write subscribed as the opposite of unsubscribed.

Then download Resend’s suppression list (Suppressions → Download CSV). It holds addresses that bounced or complained. Add each one to your import with subscribed set to false.

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.
contacts.csv
email,firstname,lastname,subscribed
ana@acme.com,Ana,Ruiz,true
tom@northwind.io,Tom,Berg,false
old@contoso.com,,,false

3. Move templates

Resend has no template export in the dashboard, but GET /templates/{id} returns each template’s html, subject and variables. Create a template in Plunk, switch the editor to HTML and paste it in.

Plunk renders variables with Liquid. Resend’s {{{PRODUCT}}} becomes {{PRODUCT}}. Its reserved keys map to contact fields: {{{FIRST_NAME}}} to whatever you named that CSV column, lowercased, such as {{firstname}}, {{{EMAIL}}} to {{email}} and {{{UNSUBSCRIBE_URL}}} to {{unsubscribeUrl}}.

4. Change your sending code

Resend fieldPlunk fieldNotes
fromfromA string, or {name, email}
totoA string, {name, email} or an array; each address gets its own email
subjectsubject
htmlbody
text(none)Plunk builds the plain-text part from the HTML
reply_toreplyA single address
headersheaders
attachmentsattachmentsfilename, base64 content, contentType; 10 files and 10 MB by default
template {id, variables}template, datadata also fills contact fields used in the template
Idempotency-Key headerIdempotency-Key headerSame idea, kept for 24 hours
cc, bcc, scheduled_at, tags, topic_id(none)Not supported on the send endpoint
Before: Resend
await resend.emails.send({
  from: 'Acme <hello@acme.com>',
  to: 'ana@acme.com',
  subject: 'Welcome to Acme',
  html: '<p>Thanks for signing up.</p>',
  replyTo: 'support@acme.com',
});
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({
    from: {name: 'Acme', email: 'hello@acme.com'},
    to: 'ana@acme.com',
    subject: 'Welcome to Acme',
    body: '<p>Thanks for signing up.</p>',
    reply: 'support@acme.com',
  }),
});

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

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

SMTP settings
# Before (Resend)
host:     smtp.resend.com
username: resend
password: your Resend 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

Resend’s API returns each automation as JSON (GET /automations/{id}), which is a useful checklist, but there is no import: you rebuild each one as a Plunk workflow. Most steps have a direct counterpart:

  • Send Email

    Send email step, using a Plunk template.
  • Delay

    Delay step, in minutes, hours or days.
  • Wait for Event

    Wait for event step.
  • Condition

    Condition step, with as many branches as you need.
  • Contact Update

    Update contact step.
  • Add to Segment

    Set a contact field with an Update contact step, and build a dynamic segment on that field.

Events you send to Resend with POST /events/send go to Plunk’s POST /v1/track instead, with email, event and an optional data object. The track endpoint accepts your public key, so it can be called from the browser.

6. Rebuild webhooks

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.

Resend eventPlunk event
email.sentemail.sent
email.deliveredemail.delivery
email.openedemail.open
email.clickedemail.click
email.bouncedemail.bounce
email.complainedemail.complaint
email.receivedemail.received
contact.updated (unsubscribes)contact.unsubscribed

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. Resend stays available as a fallback the whole time.
  3. Remove the old records last

    Once nothing sends through Resend 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 Resend 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.