Migrate from SparkPost to emailr_
Move from SparkPost to emailr_: replace transmissions, map substitution data, campaigns, metadata, suppressions and webhook events step by step.
SparkPost sends everything as a transmission: content plus a recipient list plus substitution data. emailr_ splits that into single sends through the API and audience sends through Broadcasts, which keeps each call easy to read and retry.
This guide covers the sparkpost Node client, stored templates, substitution data, the suppression list and webhooks.
The code change
A typical SparkPost send call next to its emailr_ equivalent.
import SparkPost from 'sparkpost'
const client = new SparkPost(process.env.SPARKPOST_API_KEY)
await client.transmissions.send({
campaign_id: 'receipts',
content: {
from: 'hello@mail.acme.com',
subject: 'Your receipt',
html: '<p>Thanks for your order.</p>',
},
recipients: [{ address: 'user@example.com' }],
metadata: { order_id: '123' },
})import { Emailr } from 'emailr'
const emailr = new Emailr({ apiKey: process.env.EMAILR_API_KEY! })
await emailr.emails.send({
from: 'hello@mail.acme.com',
to: 'user@example.com',
subject: 'Your receipt',
html: '<p>Thanks for your order.</p>',
tags: { campaign: 'receipts', order_id: '123' },
})SparkPost concepts in emailr_
Use this table to find where each feature you rely on lives in emailr_.
| SparkPost | emailr_ |
|---|---|
| API key | API key (et_live_ prefix) |
| Sending domain + bounce domain | Domain with send. MAIL FROM subdomain |
| Transmissions (single recipient) | emails.send() |
| Transmissions to a recipient list | Broadcasts to a segment |
Stored templates + substitution_data | Templates (template_id + template_data) |
campaign_id + metadata | tags object |
| Suppression list | Contacts with subscribed: false |
| Subaccounts | Domain workspaces |
| Relay webhooks (inbound) | Inboxes and email.received webhooks |
How to migrate from SparkPost
- 01
Create an API key
Create a key under API keys in the dashboard. Keys start with
et_live_and are scoped to one domain workspace. Store it asEMAILR_API_KEYin your secret manager or environment, never in client-side code. - 02
Add and verify your sending domain
In the dashboard, open Domains and add the domain you send from. A subdomain such as
mail.yourcompany.comkeeps transactional reputation separate from your corporate mailbox.Emailr generates the DNS records: a DKIM TXT record on the
emailr._domainkeyselector, an SPF TXT record, and an MX + TXT pair on thesendsubdomain used as the MAIL FROM / Return-Path. A DMARC record is optional but recommended.SparkPost uses a
scph…._domainkeyselector and a separate bounce domain CNAME. Keep them until you finish the cutover.Emailr's DKIM selector and
sendsubdomain don't collide with other providers' records, so you can add them before cutting over and send from both providers during the migration. A domain can only have one SPF record: if one already exists, merge Emailr'sinclude:into it instead of adding a second record. - 03
Replace transmissions
Install the SDK. For one-to-one messages, replace
transmissions.send()withemails.send(): thecontentobject'sfrom,subject,htmlandtextmove to the top level andrecipients[].addressbecomesto.campaign_idandmetadatamove intotags.For transmissions sent to a stored recipient list, import the list as contacts, group them in a segment and send a Broadcast instead.
Terminalbashnpm install emailr - 04
Move stored templates
Recreate templates in emailr_ and pass values in
template_datainstead ofsubstitution_data.Emailr templates substitute flat
{{variable}}tokens fromtemplate_data. There are no loops or conditionals, so pre-render lists and optional blocks in your code and pass the result as a variable. SparkPost's{{if}}and{{each}}blocks need the same treatment. - 05
Move the suppression list
Export the SparkPost suppression list and import the addresses as contacts with
subscribed: falseso broadcasts skip them. - 06
Recreate your event webhooks
SparkPost batches events into arrays of
msysobjects. emailr_ sends one event per request with a flat{ event, data }body.Create a webhook of type
transactional(delivery and engagement events),domain(verification status) orreceiving(inbound email). Each webhook has its own signing secret. Every request carries anX-Emailr-Signatureheader: a hex-encoded HMAC-SHA256 of the raw request body. Verify it before trusting the payload.verify-webhook.tstsimport { createHmac, timingSafeEqual } from 'node:crypto' // rawBody must be the exact request body string, before JSON.parse export function isValidEmailrWebhook(rawBody: string, signature: string | null) { if (!signature) return false const expected = createHmac('sha256', process.env.EMAILR_WEBHOOK_SECRET!) .update(rawBody) .digest('hex') const a = Buffer.from(expected) const b = Buffer.from(signature) return a.length === b.length && timingSafeEqual(a, b) } // const ok = isValidEmailrWebhook(body, req.headers.get('x-emailr-signature')) // const { event, data } = JSON.parse(body) // e.g. event === 'email.bounced'
Webhook event mapping
emailr_ webhooks post one JSON event per request with an event name and a data object. Translate event names in your handler with this table.
| SparkPost event | emailr_ event |
|---|---|
| injection | email.sent |
| delivery | email.delivered |
| bounce | email.bounced |
| spam_complaint | email.complained |
| policy_rejection | email.failed |
| initial_open / open | email.opened |
| click | email.clicked |
Things to watch for
- Per-recipient
substitution_datainside one transmission isn't supported; send per recipient or use Broadcasts with contact properties. - SparkPost's EU and US hosts are separate; emailr_ uses a single API base URL.
- Remove SparkPost's DNS records only after your last transmission has been sent.
Frequently asked questions
What replaces SparkPost transmissions?
Single messages use the emails API. Sends to a recipient list become Broadcasts to a segment of contacts, which also handle unsubscribe links.
Does emailr_ support SparkPost template logic?
No. emailr_ templates substitute flat {{variable}} values. Conditional and loop blocks need to be rendered in your code and passed in as variables.
How do campaign_id and metadata map?
Put them in the tags object on the send request, for example tags: { campaign: 'receipts', order_id: '123' }.
Can I keep my SparkPost bounce domain?
emailr_ creates its own MAIL FROM on the send subdomain of your sending domain. Keep SparkPost's bounce domain until you've finished migrating, then remove it.
Keep going
Documentation
Related guides
- Migrate from SendGridMove from SendGrid to emailr_: map dynamic templates, categories, suppressions and Event Webhook events, swap the SDK and cut over with zero downtime.
- Migrate from MailgunSwitch from Mailgun to emailr_: replace mailgun.js, map tags, variables, templates and webhook events, move suppressions and cut over without downtime.
- Migrate from Amazon SESReplace raw Amazon SES with emailr_: keep SES-grade delivery, drop SNS plumbing and configuration sets, and get templates, webhooks and an inbox.
Ready to leave SparkPost?
Create a free account, verify your domain and send your first email through emailr_ in minutes. No credit card required.