← Back to docs

Migrate from Postmark

Move transactional email from Postmark to EuroMail: API field mapping, code before/after, SMTP cutover, DNS, webhooks, and a zero-downtime checklist.

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

PostmarkEuroMail
EndpointPOST https://api.postmarkapp.com/emailPOST https://api.euromail.dev/v1/emails
Auth headerX-Postmark-Server-Token: <token>Authorization: Bearer em_live_... (X-EuroMail-Api-Key: em_live_... also works)
Sender/recipientFrom, To, Cc, Bcc, ReplyTo (PascalCase)from, to, cc, bcc, reply_to (snake_case)
HTML bodyHtmlBodyhtml_body
Text bodyTextBodytext_body
AttachmentsAttachments: [{ Name, Content, ContentType }] (base64)attachments: [{ filename, content, content_type }] (base64). Same shape, different casing
GroupingTag (single string) + Metadata (object)tags (array of strings) + metadata (object). EuroMail allows multiple tags per email
Stream selectionMessageStream (e.g. "outbound", "broadcast")Transactional and marketing sends are separate endpoint families. See Newsletters for the broadcast equivalent
IdempotencyNone. Postmark recommends deduplicating on the returned MessageIDidempotency_key request field. Same key always returns the original result, no client-side dedup needed
Success response200 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 "Authorization: Bearer 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.sendEmail({
  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

SettingPostmarkEuroMail
Hostsmtp.postmarkapp.comsmtp.euromail.dev
Port587, 2525, or 25587 (STARTTLS) or 465 (implicit TLS)
Usernameyour Server API Token (both fields)apikey (literal string)
Passwordyour 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

  1. Add your domain in the EuroMail dashboard and publish its DKIM, SPF, and DMARC records. Domain Verification.
  2. Keep Postmark's records in place during the dual-run. Different DKIM selectors and stacked SPF include:s let both providers verify at once.
  3. Remove Postmark's records after cutover, on your own schedule.

Webhook event mapping

Postmark webhookEuroMail event
Deliverysent
Bounce (hard)bounced
Bounce (soft/transient)deferred
SpamComplaintcomplained
Openopened
Clickclicked
SubscriptionChangeHandled via suppression lists, not a webhook event
SMTP API ErrorNo 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 import them in bulk:

curl -X POST https://api.euromail.dev/v1/suppressions/import \
  -H "Authorization: Bearer em_live_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "emails": ["[email protected]", "[email protected]"],
    "reason": "migrated from Postmark: suppressed"
  }'

One request takes up to 10,000 addresses, so split a bigger export into chunks. The response reports how many were added and lists any address it could not parse. The key needs the contacts:manage scope.

Details in Suppression Lists.

Your first 24 hours

Every new account spends its first 24 hours -- counted from account creation, not from your first send -- inside a probation window with two limits designed to stop day-one spam blasts without starving a real migration:

  • A send-rate cap: 5 emails per minute and 25 per hour on day one, loosening over the first week.
  • A recipient allowance: a token bucket of 20 that refills at 2 per hour, counted per deliverable recipient (to plus every cc and bcc). A settled, non-zero invoice raises the allowance to 1,000 -- a paid plan still inside its trial does not count as a payment. The allowance lifts entirely once the account is 24 hours old.

A refused send returns 429 with a retry-after header (the SMTP relay returns 452, which well-behaved mailers retry). Test sends from a not-yet-verified domain to your own address (sandbox mode) never consume the allowance, so integration testing is free.

The practical consequence for a migration: create your EuroMail account and verify your domain at least a day before cutover, so the recipient allowance has expired by the time production traffic moves. If you must cut over with real volume on day one, settle your first invoice first (1,000 recipients covers a typical day) or keep Postmark running in parallel.

After that, the account's age and payments set the pace:

WhenSend rateMonthly volume
First 24 hours5/minute, 25/hour, plus the recipient allowance above3,000
Days 2 to 715/minute, 75/hour3,000 until your first invoice is paid
From day 8100/minute, 500/hourYour plan's volume once an invoice has been paid

A paid plan that is still in its trial counts as unpaid: until a non-zero invoice has been paid, the account is held to the free allowance of 3,000 emails a month whatever plan it is on. Sends over a limit return 429. Plan the cutover around these numbers: if your normal hourly volume is above the rate for the week you cut over in, keep the old provider carrying part of the traffic until it fits.

Honest gaps

  • Message Streams are structural, not a toggle. Postmark's MessageStream field routes one API to two different sending contexts (transactional vs. broadcast) with independent suppression lists. EuroMail keeps these as genuinely separate systems. /v1/emails for 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

  1. Create a EuroMail account at least 24 hours before cutover (probation limits expire with account age) and an API key with the emails:send and contacts:manage scopes (API Keys).
  2. Verify your sending domain; confirm DKIM/SPF/DMARC show green.
  3. Register your webhook endpoint. Add HMAC verification, since Postmark never had it.
  4. Import suppressions from your Postmark export with POST /v1/suppressions/import.
  5. Switch the endpoint, auth header, and PascalCase → snake_case field names (or the SMTP username/password), accepting 202 as success.
  6. Check your volume against the limits for the week you cut over in (see above), and pay the first invoice if you need more than 3,000 emails this month. For bulk jobs, send up to 500 messages per request with POST /v1/emails/batch and back off on 429 (wait for retry-after when the response has one).
  7. Send a staging email end-to-end; confirm the sent webhook arrives.
  8. Cut production traffic over; watch the first hour in Analytics and Deliverability Insights.
  9. Keep Postmark read-only for a rollback window, then retire it.

Send your first email in about 90 seconds

The free tier includes 3,000 emails a month. Email storage and AI processing in Finland, with a DPA on every plan.

Create free account See live delivery data