What changes, what doesn't
Both are deliverability-focused, JSON-first transactional APIs with a similar philosophy. A strict separation between transactional and broadcast sending, and no tolerance for spammy behavior. The request shape is close enough that most of this migration is renaming fields. The honest comparison, including where Postmark is the better choice (its delivery visibility is genuinely excellent), is at euromail vs Postmark.
Verify your domain on EuroMail while Postmark still handles production traffic, then cut over.
API mapping
| Postmark | EuroMail | |
|---|---|---|
| Endpoint | POST https://api.postmarkapp.com/email | POST https://api.euromail.dev/v1/emails |
| Auth header | X-Postmark-Server-Token: <token> | X-EuroMail-Api-Key: em_live_... |
| Sender/recipient | From, To, Cc, Bcc, ReplyTo (PascalCase) | from, to, cc, bcc, reply_to (snake_case) |
| HTML body | HtmlBody | html_body |
| Text body | TextBody | text_body |
| Attachments | Attachments: [{ Name, Content, ContentType }] (base64) | attachments: [{ filename, content, content_type }] (base64). Same shape, different casing |
| Grouping | Tag (single string) + Metadata (object) | tags (array of strings) + metadata (object). EuroMail allows multiple tags per email |
| Stream selection | MessageStream (e.g. "outbound", "broadcast") | Transactional and marketing sends are separate endpoint families. See Newsletters for the broadcast equivalent |
| Idempotency | None. Postmark recommends deduplicating on the returned MessageID | idempotency_key request field. Same key always returns the original result, no client-side dedup needed |
| Success response | 200 with { ErrorCode, Message, MessageID, SubmittedAt, To } | 202 Accepted with { id, status: "queued", ... } |
The most consequential difference isn't a field name: EuroMail's
idempotency_key prevents duplicate sends server-side, whereas Postmark's
approach is client-side (store MessageID, check before retrying). If your
integration retries on timeout, this closes a gap you may have been handling
manually.
Code before and after
curl:
# Before: Postmark
curl -X POST https://api.postmarkapp.com/email \
-H "X-Postmark-Server-Token: your-server-token" \
-H "Content-Type: application/json" \
-d '{
"From": "[email protected]",
"To": "[email protected]",
"Subject": "Welcome aboard",
"HtmlBody": "<h1>Welcome!</h1>",
"MessageStream": "outbound"
}'
# After: EuroMail
curl -X POST https://api.euromail.dev/v1/emails \
-H "X-EuroMail-Api-Key: em_live_your_key_here" \
-H "Content-Type: application/json" \
-d '{
"from": "[email protected]",
"to": "[email protected]",
"subject": "Welcome aboard",
"html_body": "<h1>Welcome!</h1>"
}'
TypeScript:
// Before: Postmark
import { ServerClient } from "postmark";
const client = new ServerClient(process.env.POSTMARK_SERVER_TOKEN!);
await client.sendEmail({
From: "[email protected]",
To: "[email protected]",
Subject: "Welcome aboard",
HtmlBody: "<h1>Welcome!</h1>",
});
// After: EuroMail
import { EuroMail } from "@euromail/sdk";
const euromail = new EuroMail({ apiKey: process.env.EUROMAIL_API_KEY! });
await euromail.emails.send({
from: "[email protected]",
to: "[email protected]",
subject: "Welcome aboard",
html_body: "<h1>Welcome!</h1>",
});
Python, Rust, and Go SDKs follow the same field names as the REST API.
SMTP cutover
| Setting | Postmark | EuroMail |
|---|---|---|
| Host | smtp.postmarkapp.com | smtp.euromail.dev |
| Port | 587, 2525, or 25 | 587 (STARTTLS) or 465 (implicit TLS) |
| Username | your Server API Token (both fields) | apikey (literal string) |
| Password | your Server API Token (same value as username) | your EuroMail API key (em_live_...) |
EuroMail follows the SendGrid/SES convention of a literal apikey username
with the key as the password, rather than Postmark's token-as-both-fields
pattern. A two-field change in your SMTP config. Full walkthroughs in
Send Email via SMTP.
DNS: verify your domain before switching
- Add your domain in the EuroMail dashboard and publish its DKIM, SPF, and DMARC records. Domain Verification.
- Keep Postmark's records in place during the dual-run. Different DKIM
selectors and stacked SPF
include:s let both providers verify at once. - Remove Postmark's records after cutover, on your own schedule.
Webhook event mapping
| Postmark webhook | EuroMail event |
|---|---|
Delivery | delivered |
Bounce (hard) | bounced |
Bounce (soft/transient) | deferred |
SpamComplaint | complained |
Open | opened |
Click | clicked |
SubscriptionChange | Handled via suppression lists, not a webhook event |
SMTP API Error | No equivalent. Malformed EuroMail requests are rejected synchronously with a 4xx response, not reported asynchronously |
Signature verification changes the most here: Postmark doesn't support HMAC
webhook signatures at all. It recommends Basic Auth on the callback URL or
IP allowlisting instead. EuroMail signs every payload with HMAC-SHA256 in the
X-EuroMail-Signature header, which is a strictly stronger guarantee. You
can drop the URL-embedded credentials or IP allowlist and verify the payload
itself. See Webhooks.
Bring your suppression list
Export bounces and spam complaints from the Postmark dashboard, then replay them:
curl -X POST https://api.euromail.dev/v1/suppressions \
-H "X-EuroMail-Api-Key: em_live_your_key_here" \
-H "Content-Type: application/json" \
-d '{
"email_address": "[email protected]",
"reason": "migrated from Postmark: hard bounce"
}'
Details in Suppression Lists.
Honest gaps
- Message Streams are structural, not a toggle. Postmark's
MessageStreamfield routes one API to two different sending contexts (transactional vs. broadcast) with independent suppression lists. EuroMail keeps these as genuinely separate systems./v1/emailsfor transactional, Newsletters for broadcast, with their own suppression semantics, matching Postmark's isolation but through different endpoints rather than a request field. - Bounce/spam hooks feeding external tools (e.g. a CRM synced via Postmark's granular bounce types) will need their handler updated for EuroMail's event names. See the mapping table above.
- Full comparison at euromail vs Postmark.
Cutover checklist
- Create a EuroMail account and an API key with the
emails:sendscope (API Keys). - Verify your sending domain; confirm DKIM/SPF/DMARC show green.
- Register your webhook endpoint. Add HMAC verification, since Postmark never had it.
- Import suppressions from your Postmark export.
- Switch the endpoint, auth header, and PascalCase → snake_case field names
(or the SMTP username/password), accepting
202as success. - Send a staging email end-to-end; confirm the
deliveredwebhook arrives. - Cut production traffic over; watch the first hour in Analytics and Deliverability Insights.
- Keep Postmark read-only for a rollback window, then retire it.