Migrate from Postmark
Keep your Postmark client and change the host and token, or adopt our SDK. Your ErrorCode handling keeps working, 406 included.
Facts verified 2026-07-17 — corrections: hello@emailfast.dev
Two ways to switch
Email Fast speaks Postmark's send API natively: SendGrid-, Mailgun-, and Postmark-compatible endpoints: point your existing SDK at a new base URL with a new key and keep your code — attachment sends are the one exception, refused with a clear error in your provider's own format rather than silently dropped. The semantics your retry and suppression code depends on are preserved: ErrorCode: 0 on success, 406 for an inactive (suppressed) recipient, 300 for a blocked send — a failing single send answers HTTP 422 with the ErrorCode in the body, Postmark's real wire behavior — and /email/batch always returns HTTP 200 with per-message results.
Path 1 — keep the postmark package
import { ServerClient } from "postmark";
- const client = new ServerClient(process.env.POSTMARK_SERVER_TOKEN);
+ const client = new ServerClient(process.env.EMAILFAST_API_KEY, {
+ requestHost: "api.emailfast.dev", // ef_sandbox_… works everywhere
+ });
await client.sendEmail({
From: "you@yourdomain.com",
To: "ada@example.com",
Subject: "Welcome",
HtmlBody: "<h1>Hi Ada</h1>",
});Calling over raw HTTP? Add an Idempotency-Key header and retries become safe:
curl https://api.emailfast.dev/email \
-H "X-Postmark-Server-Token: ef_sandbox_..." \
-H "Idempotency-Key: welcome-ada-1" \
-H "Content-Type: application/json" \
-d '{ "From": "you@yourdomain.com", "To": "ada@example.com",
"Subject": "Welcome", "HtmlBody": "<h1>Hi Ada</h1>" }'Path 2 — adopt @email-fast/nodejs
- await client.sendEmail({
- From: "you@yourdomain.com",
- To: "ada@example.com",
- Subject: "Welcome",
- HtmlBody: "<h1>Hi Ada</h1>",
- });
+ const ef = new EmailFast({ apiKey: process.env.EMAILFAST_API_KEY, baseUrl: "https://api.emailfast.dev" });
+ await ef.send({
+ to: "ada@example.com",
+ subject: "Welcome",
+ html: "<h1>Hi Ada</h1>",
+ idempotency_key: "welcome-ada-1",
+ });What maps 1:1
Both endpoints, the server-token header, per-message results on batch, Cc/Bcc carried on every message fanned from a comma-separated To, and the ErrorCode contract — including the 406 your code already treats as "stop sending to this address." One thing deliberately does not map: outbound attachments aren't supported yet, platform-wide, and a send carrying one answers HTTP 422 with ErrorCode 300 — never silently stripped. Every message admitted this way passes the same checkpoint as every other ingress: every send — REST, SMTP, browser SDK, compatibility endpoints, broadcasts, automations — passes through one admission gate: idempotency, suppression, quota, and content policy in a single checkpoint no ingress can skip.
What moves by hand today
There is no hosted Postmark importer — the planned import lane covers SendGrid, Mailgun, and Mailchimp. Templates re-create in the dashboard or over POST /v1/templates: slug each with your old TemplateId or alias and existing TemplateId/TemplateAlias sends resolve to it unchanged. Suppression lists move by CSV. Today you can evaluate the send path end to end with a sandbox key: sandbox keys (ef_sandbox_…) that run the real pipeline dry: real validation, real rendering, real events, a hosted capture inbox — and no email leaves.
The idempotency bonus
Pass an Idempotency-Key header and each message in a /email/batch call gets its own derived key, so a timeout-and-retry can never double-send part of a batch: a 202 from the API means the send is committed to a durable, partitioned outbox before we answer — a crash can't lose it, and a retry with the same idempotency key can't double-send.
Next
Questions, answered plainly
Do I need to change my application code to migrate from Postmark?
No. POST /email and POST /email/batch are accepted as-is. Put your Email Fast key in the X-Postmark-Server-Token header and change the host — the response bodies stay Postmark-shaped.
Does my ErrorCode handling still work?
Yes. Success returns ErrorCode: 0 with a MessageID; a failing single send answers HTTP 422 with the ErrorCode in the body — 406 for a suppressed recipient, 300 for a blocked send — real Postmark wire behavior, so the official postmark npm client's typed errors work. /email/batch always answers HTTP 200 with a per-message result array, exactly as Postmark does.
How do I test before pointing production at Email Fast?
Use a sandbox key (ef_sandbox_…) as the server token. The full pipeline runs — validation, rendering, events, a hosted capture inbox — and no email leaves.