emails.sh vs Mailgun

Mailgun, owned by Sinch, pairs a sending API with routing, validation, and analytics. emails.sh is a smaller surface with stored, threaded inbound.

Transactional email APIs · Mailgun claims checked July 2026

What Mailgun is

Mailgun was one of the first email APIs a developer could sign up for without a sales call, and it is now owned by Sinch, which also owns Mailjet and Email on Acid. Its distinguishing feature has always been Routes: inbound rules that match on recipient or headers and then forward, store, or POST the message somewhere. Around the send it has added address validation, deliverability analytics, inbox placement testing, and dedicated IPs, so the account you get today is a suite rather than a single endpoint.

Where Mailgun is the better choice

Pick Mailgun if inbound routing rules are doing real work for you, if you want address validation and inbox placement testing from the same vendor, or if you are already inside Sinch for SMS and want the consolidated relationship. Its routing engine is more expressive than anything we offer: match on arbitrary headers, chain actions, and fan a message out to several destinations.

Where emails.sh differs

Mailgun gives you a routing rule and a webhook. We give you a mailbox: the message is stored, parsed, threaded against what you sent, and available to read and reply to over the API. On the send side the difference is surface area. One endpoint, one key, nine language clients, and an integration an assistant can finish in a turn because the whole site is machine-readable and there is a skill it can install.

01

Stored conversations, not routing rules

A Mailgun Route fires an action and the message is gone unless you stored it. Here inbound mail is kept, threaded on In-Reply-To and References, and replyable through the API. For a support or reply-to-notification flow that is the difference between a webhook handler and a feature.

02

A smaller thing to hold in your head

Sending is one endpoint with one body shape. There is no separate validation product, no analytics module, and no regional API host to pick before your first request. Fewer choices at the start is worth more than more features you will not switch on.

03

Written for the assistant doing the work

Add /llms.txt or install the Cursor and Claude Code skill and your assistant writes a correct send call, a domain verification step, and a signed webhook handler without fetching a page. Our errors are prose with the next action in them, which is what an agent reads and acts on.

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.
Mailgun Volume plans, with validation, analytics, and dedicated IPs priced on top.
Free tier
emails.sh 3,000 emails a month, 100 a day, no card, sending from your own verified domain.
Mailgun A small daily allowance, with trials on the higher plans.
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.
Mailgun Per-domain setup with DKIM and tracking CNAMEs, plus EU and US regions.
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.
Mailgun Long-established, with inbox placement testing and a deliverability services arm.
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.
Mailgun Routes: expressive matching rules that forward, store, or POST a message.
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.
Mailgun Detailed logs and analytics, with retention that varies 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.
Mailgun Stored templates with versioning and Handlebars substitution.
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.
Mailgun Official and community libraries across 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.
Mailgun Mailing lists as an alias address with a members API. No campaign product; Mailgun points marketing users at Mailjet.
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.
Mailgun Signed events for delivery, bounce, complaint, open, click, and unsubscribe.
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.
Mailgun Extensive human docs. 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.
Mailgun Tiered by plan, with faster response and deliverability help higher up.

You do not have to rewrite the send call

emails.sh serves a Mailgun-compatible API that takes their form-encoded send at /v3/<domain>/messages with the same HTTP basic auth, and answers {"id":"<...>","message":"Queued. Thank you."} with a 200, which is what their schema declares required and what their own SDK fixtures assert. Errors come back as {"message":"..."}, the single field their error schema documents. Keep the package, keep every call, and change the one line that says where it points.

The entire change
// Node, mailgun.js
const mg = new Mailgun(FormData).client({
  username: 'api',
  key: 'esh_live_yourkey',
  url: 'https://api.emails.sh/mailgun'
});

All seven of their clients are overridable, and only the Go one reads an environment variable (MG_URL, and only through NewMailgunFromEnv). Go v4 wants /v3 on the end of the base URL and v5 errors if you include it. recipient-variables is the field a migration trips on: batch sending with per-recipient substitution becomes POST /v1/emails/batch, up to 100 fully rendered messages, which also means each recipient gets their own message rather than seeing the others in the To header. o:testmode and o:tracking-clicks are refused with a message naming the substitute. Import your suppression lists first, and twice if you have EU domains: suppressions are region-bound at Mailgun.

Read the migration chapter

Moving from Mailgun

The compatibility endpoint handles the code, so this is no longer a rewrite of the send function. `npx @emails.sh/cli migrate mailgun` does the account: it reads bounces, complaints, and unsubscribes for every domain, plus domains, webhooks, and templates, prints a plan, and waits for a yes. Routes are the part that still needs thought.

  1. 01

    Import the suppression lists first, from every region

    Before anything else, and this is the step that is not reversible. Mailgun keeps bounces, complaints, and unsubscribes per domain, and they are region-bound: keys and domain names replicate between api.mailgun.net and api.eu.mailgun.net, but suppressions do not. Run migrate mailgun, then again with --region eu if you have EU domains, or every EU bounce is left behind and you will mail those people from a brand new domain.

  2. 02

    Point the client here, or swap it

    Set the url option to https://api.emails.sh/mailgun and the key to an esh_ one, and nothing else changes. If you would rather move properly, install @emails.sh/sdk: a JSON body instead of a form, to as an array rather than a repeated field, h: headers as an object, and v: variables as tags.

  3. 03

    Decide what each Route was for

    Routes that forward to a human address can keep running at Mailgun until you are ready. Routes that POST to your app become inbound addresses here, where the message is stored and threaded rather than handed over once.

  4. 04

    Re-verify the domain

    Add your sending domain here and publish our DKIM and return-path records. Mailgun's records can stay while you run both. There is no reputation to rebuild, because reputation attaches to the domain and the domain is not changing.

  5. 05

    Cut over sending first, MX last

    Move a percentage of sends here and compare bounces and complaints for a week. Only change the MX record when you are satisfied, because inbound can only point at one provider and that switch is the one that is hard to stage.

Questions

Can I keep using the Mailgun client library?

Yes. We serve a Mailgun-compatible API at https://api.emails.sh/mailgun that takes their form-encoded send at /v3/<domain>/messages with the same HTTP basic auth, and answers {"id":"...","message":"Queued. Thank you."} as theirs does. All seven of their official clients take a base URL, so it is one line: in Node it is the url option on client(). Go v4 needs /v3 on the end and Go v5 rejects it, which is the one thing to get right.

What is the first thing to do when migrating?

Import the suppression lists, and check your regions while you do it. Mailgun keeps bounces, complaints, and unsubscribes per domain, and they are region-bound: keys and domain names replicate between the US and EU hosts but suppressions do not. Run `npx @emails.sh/cli migrate mailgun`, then again with --region eu if any of your domains are there. Their lists page on an opaque cursor rather than an offset, so an importer using skip silently stops after the first page; ours follows the cursor.

Who owns Mailgun?

Sinch, which also owns Mailjet and Email on Acid. Mailgun is part of a communications group rather than an independent product, which shapes its roadmap and its enterprise packaging.

Are Mailgun Routes better than emails.sh inbound?

For pure routing, yes. Routes match on arbitrary headers and chain actions, which we do not do. What we do instead is keep the message: parsed, threaded, searchable, and replyable, which is what you want if the reply is part of your product rather than a message to forward on.

How different is the API?

Different enough to be a rewrite of the send function, not different enough to be a project. Mailgun takes form-encoded fields with basic auth; we take a JSON body with a bearer token. The field names map almost directly, and to becomes an array.

Can I keep Mailgun for inbound and use emails.sh to send?

Yes, and it is the safest order. MX points at exactly one provider, so leave inbound where it is, prove the send path here, then move receiving when you have a reason to.

Looking for a Mailgun 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