Migrate from Postmark
Postmark and eusend have the same shape — an HTTP API for transactional email, an SMTP door for everything else, and webhooks for delivery events — so a typical migration is an afternoon, most of it waiting on DNS. What you gain is an EU-native provider: your mail is sent and stored on infrastructure in the EU, operated by an EU company, with no US parent in the chain.
The code change
Install the SDK with npm install @eusend_dev/sdk. The main difference you'll notice is casing — Postmark's fields are PascalCase, eusend's are camelCase — and that eusend returns { data, error } instead of throwing on failure.
import postmark from 'postmark'
const client = new postmark.ServerClient(
process.env.POSTMARK_SERVER_TOKEN
)
await client.sendEmail({
From: 'Acme <hello@acme.com>',
To: 'user@example.com',
Subject: 'Welcome',
HtmlBody: '<p>Thanks for signing up.</p>',
MessageStream: 'outbound',
})import { Eusend } from '@eusend_dev/sdk'
const eusend = new Eusend(
process.env.EUSEND_API_KEY
)
await eusend.emails.send({
from: 'Acme <hello@acme.com>',
to: 'user@example.com',
subject: 'Welcome',
html: '<p>Thanks for signing up.</p>',
})Field mapping
| Postmark | eusend | Notes |
|---|---|---|
| From | from | Must use a domain you verified in eusend |
| To / Cc / Bcc | to / cc / bcc | Postmark takes comma-separated strings; eusend takes a string or an array |
| ReplyTo | replyTo | String or array |
| Subject | subject | — |
| HtmlBody / TextBody | html / text | — |
| Headers ([{ Name, Value }]) | headers | A plain object instead of an array of name/value pairs |
| TrackOpens / TrackLinks | trackOpens / trackClicks | Both default to true on eusend |
| MessageStream | — | No equivalent needed. Transactional sends go through the API; campaigns are a separate Broadcasts feature. |
| Tag / Metadata | — | No direct equivalent yet. Custom headers are the usual workaround. |
Batch sending maps one-to-one: client.sendEmailBatch([...]) becomes eusend.batch.send([...]), up to 100 messages per request. If you use Postmark templates, eusend has server-side templates too — pass templateId instead of a body; see the docs for the variable syntax.
If you send over SMTP
Anything pointed at Postmark's SMTP endpoint moves by swapping credentials — no code change. One thing to get right: eusend uses implicit TLS on port 465 (the connection is encrypted from the first byte); STARTTLS on 587 is not supported.
// Before: smtp.postmarkapp.com:587, server token as user and pass
// After — note the port: eusend is implicit TLS on 465, not STARTTLS on 587
const transport = nodemailer.createTransport({
host: 'smtp.eusend.dev',
port: 465,
secure: true,
auth: { user: 'eusend', pass: process.env.EUSEND_API_KEY },
})Webhooks
Postmark gives each event type its own webhook; eusend sends all events to one endpoint and you subscribe to the types you want. The events map directly:
| Postmark webhook | eusend event |
|---|---|
| Delivery | email.delivered |
| Bounce | email.bounced |
| Spam complaint | email.complained |
| Open | email.opened |
| Click | email.clicked |
Where Postmark relies on HTTP basic auth or IP allowlists to protect the endpoint, every eusend webhook is signed with HMAC-SHA256 — verify the signature and you're done. The webhook docs show the exact scheme.
What's honestly different
- Attachments carry over. Postmark's
Attachmentsarray maps to eusend'sattachments— renameName→filename,Content→content(still base64),ContentType→content_type, andContentID→content_idfor inline images. Up to 20 files, 10 MB combined per message. - Suppressions come with you conceptually, not literally. eusend suppresses hard bounces and complaints automatically, but it can't see Postmark's history. Export your suppression list and keep those addresses out of your sends.
- Broadcast-stream traffic waits. Transactional sending is open today; bulk campaigns open as the platform warms. If you use Postmark's broadcast streams, plan that part for later.
Before you flip production
- Verify your sending domain and add its DKIM, SPF, and DMARC records — the Domains page generates them. Sends from unverified domains are rejected.
- Migrate against an
eu_test_API key first — test sends are accepted and tracked but never delivered — then switch toeu_live_. - Keep your Postmark account alive for a week or two after cutover so in-flight webhooks and your suppression export have somewhere to come from.