emails.sh vs Postmark

Postmark is the deliverability-first sender with separate message streams. emails.sh compares on inbound, SDK breadth, and the agent install path.

Transactional email APIs · Postmark claims checked July 2026

What Postmark is

Postmark has spent over a decade being the answer when the requirement is "this email has to arrive". Its defining structural choice is message streams: transactional mail and broadcast mail run on separate streams with separate reputation, so a promotional campaign can never drag your password resets down with it. It publishes its own time-to-inbox figures, it has an inbound parsing service that turns received mail into a JSON webhook, and its support is famously answered by people who understand email rather than by a script.

Where Postmark is the better choice

If deliverability is the entire purchase decision and you are willing to pay for it, buy Postmark. The message stream separation is a genuinely good idea that most competitors do not have, the support quality is real, and a decade of reputation on shared pools is something no new provider can manufacture. Teams sending mail where a single lost message is a compliance problem should default to them.

Where emails.sh differs

We agree with Postmark about almost everything, including that transactional and bulk mail should not share a reputation. Where we differ is the surface around the send. Inbound here is a stored, threaded conversation rather than a webhook you persist yourself, there are nine maintained language clients instead of a narrower set, and the whole integration is designed so a coding assistant can complete it: markdown at every URL, an /llms.txt, and an installable skill.

01

Inbound is threaded, not only parsed

Postmark inbound posts parsed JSON to your endpoint and the storage is your problem. We keep the message, thread it against In-Reply-To and References, and let you fetch or reply through the API. If you want a conversation rather than a stream of events, that is the difference between building it and having it.

02

Nine language clients

Node and TypeScript, Python, PHP, Ruby, Go, Rust, Java, .NET, and Elixir, plus curl. Same method names, same error shapes, same retry behaviour. If your service is Elixir or Rust, that is the difference between an SDK and an afternoon with an HTTP library.

03

The assistant does the integration

Install the Cursor or Claude Code skill and the assistant already knows POST /v1/emails, the batch endpoint, the webhook signature header, and the domain verification flow. No doc fetch, no guessing at a parameter name, no snippet with a placeholder it cannot resolve.

Side by side

Every row is a claim we could defend with their documentation open beside it. Where they are ahead, the row says so.

Pricing shape
emails.sh Per email sent, in monthly volume bands. Domains, API keys, and seats are not billed separately.
Postmark Per email in plan tiers with overage per thousand above the included volume.
Free tier
emails.sh 3,000 emails a month, 100 a day, no card, sending from your own verified domain.
Postmark A trial allowance rather than a standing free tier.
Sending domains
emails.sh One screen: add the domain, copy SPF, DKIM, and return-path records, watch them go green. Unlimited domains on every plan.
Postmark Sender signatures and domain-level DKIM, with a clear setup flow.
Deliverability posture
emails.sh Shared pools separated by send type, bounce and complaint tripwires below the thresholds that get senders suspended, dedicated IP on request.
Postmark The strongest reputation story in the category, with published time-to-inbox data.
Receiving mail
emails.sh Inbound MX, parsed MIME, and In-Reply-To threading, so a reply arrives as part of a conversation you can query rather than as a raw webhook body.
Postmark Yes: inbound parsing to a JSON webhook. Storage and threading are yours.
Logs and retention
emails.sh Every message searchable in the dashboard and readable at GET /v1/emails/:id, with its delivery, bounce, and complaint events attached.
Postmark Detailed activity with message history retained by plan.
Templates
emails.sh Send html and text in the request, or store a template and reference it by id. No visual builder, because the markup belongs in your repo next to the code that sends it.
Postmark Hosted templates with a layout system and versioning.
SDKs and frameworks
emails.sh Nine clients (node and typescript, python, php, ruby, go, rust, java, .net, elixir) plus curl, and framework guides from Next.js to Laravel, Rails, and Django.
Postmark Official libraries for the mainstream languages.
Marketing and lifecycle
emails.sh Audiences, segments, scheduled broadcasts, and automations, on the same verified domains and the same delivery log as your transactional mail, and on a reputation pool that is kept separate from it by force.
Postmark Broadcast message streams and a bulk send endpoint. No contact list, no campaign builder, and no automations.
Webhooks
emails.sh email.delivered, email.bounced, and email.complained, each signed with x-emailssh-signature so you can verify the body before you act on it.
Postmark Delivery, bounce, spam complaint, open, click, and subscription change.
Path for a coding agent
emails.sh Every page served as markdown at the same path plus .md, an /llms.txt index of the whole site, and a Cursor and Claude Code skill you install once.
Postmark Thorough human documentation. No installable assistant skill.
Support
emails.sh Email support on every plan including the free one, answered by the people who wrote the API. We are small, and that cuts both ways.
Postmark Widely cited as excellent, answered by people who know email.

You do not have to rewrite the send call

emails.sh serves a Postmark-compatible API that speaks their exact request format, their exact five-field response (To, SubmittedAt, MessageID, ErrorCode, Message), and their exact error shape. Their official client talks to it without noticing. No Postmark SDK reads an environment variable for its host, so this is one line of configuration rather than two variables, but it is still one line: keep the package, keep the imports, keep every call.

The entire change
# PHP
PostmarkClientBase::$BASE_URL = 'https://api.emails.sh/postmark';

# .NET
new PostmarkClient(serverToken, "https://api.emails.sh/postmark");

# Python
ServerClient(server_token, base_url="https://api.emails.sh/postmark")

# Your server token becomes an esh_ key in the same header.

Four of their six clients can be pointed at a path under a host. Node and Java cannot: both take a bare hostname with no path, so use @emails.sh/sdk there. Postmark ships no Go client at all. Message streams are the one thing refused rather than mapped: we serve one transactional stream, and a send naming a broadcast stream answers their error code 1236 with a sentence saying to use a separate sending subdomain instead, because quietly putting bulk mail on a transactional path would undo the isolation you chose streams for. Import your suppression list before you send anything: those addresses hard-bounced or complained already.

Read the migration chapter

Moving from Postmark

The compatibility endpoint handles the code. These are the parts of an account that are not code, and `npx @emails.sh/cli migrate postmark` does the first three for you: it reads your suppression list, domains, webhooks, and templates from Postmark, prints a plan, and waits for a yes.

  1. 01

    Import the suppression list first

    Before anything else, and this is the step that is not reversible. Postmark keeps suppressions per message stream, so an export that dumps only the outbound stream leaves every broadcast bounce behind; the migrate command lists the streams and dumps all of them. Those addresses hard-bounced or filed a spam complaint already, and mailing them again from a new setup is the fastest way to get a domain filtered in its first week.

  2. 02

    Point the client here, or swap it

    Set the base URL to https://api.emails.sh/postmark and the server token to an esh_ key, and nothing else changes. If you would rather move properly, install @emails.sh/sdk: the send takes the same fields with our spellings, To as an array rather than a comma-separated string.

  3. 03

    Map streams onto subdomains

    A Postmark server token per environment becomes an esh_ key per environment. A broadcast stream becomes a separate sending subdomain, which is the mechanism underneath a stream either way: verify news.yourdomain.com and send campaigns from it so they cannot affect your password resets.

  4. 04

    Re-verify the sender domain

    Add the domain here and publish our DKIM and return-path records next to Postmark's. Keep both live while you run in parallel. Your SPF record can include both, and DKIM selectors do not collide.

  5. 05

    Move the inbound address last

    If you use Postmark inbound, its MX or forwarding address is the one thing you cannot run twice, since mail goes to exactly one place. Switch it after the send path is proven, and keep the old parser running for a week to catch stragglers.

Questions

Can I keep using the Postmark client library?

Yes. We serve a Postmark-compatible API at https://api.emails.sh/postmark that speaks their wire format exactly, including the five-field success body and the {ErrorCode, Message} error shape. Set the base URL there and the server token to an esh_ key. None of their SDKs reads an environment variable for the host, so it is one line of code rather than a config change, and the PHP, .NET, Python, and Ruby clients all take it. The Node and Java clients take a bare hostname with no path and cannot reach a prefixed URL, so use @emails.sh/sdk there. The full table is at https://emails.sh/docs/migrate-postmark.

What is the first thing to do when migrating?

Import the suppression list, before you send a single message. Postmark keeps suppressions per message stream, so dumping only the outbound stream leaves every broadcast bounce behind. `npx @emails.sh/cli migrate postmark` lists the streams, dumps all of them, and writes the addresses to your list here before it touches anything else. Every other step in a migration can be undone; mailing a thousand addresses that already hard-bounced cannot.

What are Postmark message streams?

Separate channels inside your account for transactional and broadcast mail, each with its own reputation, so a marketing send cannot damage delivery of your receipts. It is a good design. Here you get the same isolation by sending campaigns from a separate subdomain, which is the underlying mechanism either way.

Does Postmark receive email?

Yes. It parses inbound mail and posts JSON to your endpoint. The difference here is that we store and thread it, so replies are queryable as a conversation rather than something you have to persist and stitch together yourself.

Is Postmark more reliable than emails.sh?

It has a longer record, and a longer record is worth something real in email. We would rather you test than take either claim on faith: authenticate your domain, send a slice of production traffic through both, and compare bounce and complaint rates over a week.

Which costs less?

It depends entirely on your volume and how much of it is bulk. Postmark prices in plan tiers with per-thousand overage; we price in monthly volume bands. Take last month's send count to both pricing pages before you decide, because the crossover point moves with your shape.

Looking for a Postmark alternative?

These products all send an HTTP request and put a message in an inbox, and any of them will do that competently. The choice comes down to what surrounds the send: what the logs tell you at 2am, whether replies come back, and how long it takes an assistant to write working code against the API on the first try.

The free tier is 3,000 emails a month, 100 a day, no card, sending from your own verified domain, with delivery logs, webhooks, and inbound included. Enough to run real traffic through it before you decide.

More Transactional email APIs