Library / SDK
mailgun/mailgun.js avatar
mailgun/mailgun.js

mailgun.js is a typed Mailgun client with four bundles and two HTTP backends

Javascript SDK for Mailgun

552 stars108 forksTypeScriptApache-2.0

At a glance

What is it?
mailgun.js is the JavaScript and TypeScript SDK for the Mailgun API, published with conditional exports for CommonJS and ESM in both Node and browser form, plus a Fetch API mode for environments without XMLHttpRequest. It suits a server-side sender that wants typed wrappers over the Mailgun endpoints, and it warns browser users that a proxy and a private key are both required.
Who is it for?
mailgun.js fits a Node service that sends transactional mail through Mailgun and wants the request shapes and the response types described in TypeScript rather than assembled by hand. It does not fit a browser-only application, because the README states a proxy is required for cross-origin reasons and that publishing your private API key in frontend code is unsafe, so the browser bundle is for local demos rather than production.
Can I use it commercially?
Yes. Apache-2.0 is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 3 days ago.
What is it written in?
Mainly TypeScript, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 22, 2026, and from our analysis. They are not legal advice.

Editorial analysis

One client, one credential pair, two configuration details that matter

The SDK is described as a javascript sdk for Mailgun built with webpack, babel and es6, and it can be used in node or in the browser. The core of the API is small enough to state in full.

You import the module, construct it with a FormData implementation, and ask it for a client that takes basic auth credentials.

js
  const Mailgun = require('mailgun.js');
  const mailgun = new Mailgun(FormData); // or const formData = require('form-data');
  const mg = mailgun.client({username: 'api', key: process.env.MAILGUN_API_KEY || 'MAILGUN_API_KEY'});

The same thing with an import statement.

js
  import Mailgun from 'mailgun.js';
  const mailgun = new Mailgun(FormData); // or import formData from 'form-data';
  const mg = mailgun.client({username: 'api', key: process.env.MAILGUN_API_KEY || 'MAILGUN_API_KEY'});

Two things stand out. The username is fixed as `api` and the key is your Mailgun API key, and it is read from the environment in the examples rather than hardcoded. And the constructor requires a FormData implementation, which the README explains was introduced in version 3.0 so the library could stay universal across environments; in Node you can use the built-in FormData or the `form-data` package.

The third configuration detail is regional. The README marks it IMPORTANT: if you use EU infrastructure you need to pass `url: 'https://api.eu.mailgun.net'` together with the auth credentials. There is no mention of a region option in the quick start, so a user on the EU region who copies the short example gets a request against the wrong base URL unless they read further.

The node requirement is stated up front: node.js >= 18.x.

Four bundles, and how package.json decides which one you get

The README is explicit that there are four bundles available, all conditionally exported by package.json and separated by environment.

In the Node.js environment there are two. CommonJS, a bundle for the CommonJS module system, where the .dist/CJS/mailgun.node.cjs file is expected to be used. And ECMAScript modules, a bundle for the Node.js environment, where the .dist/ESM/mailgun.node.js file is expected to be used. The ESM bundle also supports dynamic imports.

js
      // In this case, the .dist/ESM/mailgun.node.js file is expected to be used
      const Mailgun = await import('mailgun.js');
      const mailgun = new Mailgun.default(FormData);
      ...

In the browser environment there are two more. Asynchronous Module Definition, a bundle for the Require.js module loader, which requires the loader to be present in the environment. And ECMAScript modules for the browser, where the .dist/ESM/mailgun.browser.js file is expected to be used.

html
      <script type="module">
        import Mailgun from './dist/ESM/mailgun.browser.js';
        const mailgun = new Mailgun(FormData);
        ...
      </script>

The reason four exist rather than one is the same reason FormData is a constructor argument. The library has to work where XMLHttpRequest exists and where it does not, and it has to load in a CommonJS server, an ESM server, a Require.js page and a native module browser page. Conditional exports handle the resolution so the consumer does not have to reach into the dist directory, but it does mean the resolution rules of your bundler decide which file you actually get.

That is the practical thing to check after installing. If you are debugging a missing method, the first question is which of the four files your tooling resolved.

Types, interfaces and enums live in a separate definitions submodule

The type story is a separate section of the README, and it has two halves.

Types defined by the SDK can be imported from the definitions submodule.

TS
 import { MailgunClientOptions, MessagesSendResult } from 'mailgun.js/definitions';

And interfaces and enums defined by the SDK can be imported from that same submodule.

TS
  import { Interfaces, Enums } from 'mailgun.js/definitions';
  ...
  const mailgunClient: Interfaces.IMailgunClient = mailgun.client(clientOptions);
  const yes = Enums.YesNo.YES;

So you get both a flat export of named types and a namespaced one, and the second form is what gives you the interface names in full, `Interfaces.IMailgunClient`, and the enums as values rather than as type positions.

The build configuration matches this. The docs script runs typedoc with the typedoc-plugin-markdown plugin and a tsconfig, which is how the generated API reference is produced, and the repository holds a docs/ directory and a dist/ directory that contains the built bundles. The CHANGELOG.md and a commitlint config with husky hooks sit at the root, so the commit message format is enforced in the development flow.

For a TypeScript user, the definitions submodule is the part that saves the most time. The endpoint methods return typed results such as MessagesSendResult, so you are not parsing an untyped response body to find out what came back.

The Fetch mode trades proxy and form-data for compatibility

Since version 12.1.0 the Fetch API is available for environments that do not support XMLHttpRequest, and it is enabled by a configuration parameter named useFetch.

TS
import Mailgun from 'mailgun.js';
const mailgun = new Mailgun(FormData);

const mg = mailgun.client({
  username: 'api',
  key: process.env.MAILGUN_API_KEY || 'MAILGUN_API_KEY',
  useFetch: true,
});

The README attaches a warning to it that deserves attention: proxy configuration and the `form-data` NPM package are not supported in this case. Both features work in the default mode and neither works in Fetch mode.

That is a genuine constraint rather than a cosmetic one. A corporate network where all traffic goes through an HTTP proxy is exactly the environment where you might reach for Fetch, and it is exactly where the proxy option is unavailable, so the two settings are mutually exclusive in practice.

The test setup shows the same split from the maintainer side. The main test script runs the axios module tests and then the fetch module tests, the second being the axios suite run again with USE_FETCH=true in the environment. There are separate integration configs for Node and for the browser, the Node one running jest with experimental VM modules enabled and no cache.

So the Fetch path is tested as a first class configuration rather than an afterthought, which is what you want to know before depending on it in a runtime where XMLHttpRequest is absent.

Proxy configuration and the browser warning that comes with it

The proxy block is a client configuration option, and the README shows the shape of it.

js
  import Mailgun from 'mailgun.js';
  const mailgun = new Mailgun(FormData);// or import FormData from 'form-data';

  const mg = mailgun.client({
    username: 'api',
    key: process.env.MAILGUN_API_KEY || 'MAILGUN_API_KEY',
    proxy: {
      protocol: 'https' // 'http' ,
      host: '127.0.0.1', // use your proxy host here
      port: 9000, // use your proxy port here
      auth: { // may be omitted if proxy doesn't require authentication
        username: 'user_name', // provide username
        password: 'user_password' // provide password
      }
    },
  });

The protocol accepts `https` or `http`, the auth block is optional, and the whole proxy sits alongside the username and key rather than replacing them.

Now the caveat that decides whether you use this SDK on the client at all. The README opens with a note that if the SDK is used in the browser, a proxy is required to communicate with the Mailgun API because of cross-origin limitations, and it says explicitly that you should not publish your private API key in frontend code.

Those two statements together mean the browser bundle is for local experimentation and demos, exactly as the repository's examples directory suggests with an index.html file. The examples folder is otherwise Node scripts, addresses.js, list-domains.js and send-email.js, which is the intended usage.

The other half of the browser story is subaccounts. Primary accounts can make API calls on behalf of their subaccounts, and the README links to the Mailgun API documentation for that. It is the one place where the SDK is not about a single key: with a parent account you can act for several subaccounts, which is the arrangement for services that bill customers separately but send through one Mailgun account.

Release cadence, packaging and the licensing question

The project is active. Version 14.1.0 was released on 2026-10-08, 14.0.2 the same day, and 14.0.1 on 2026-09-04, with the last push to the repository on 2026-10-11.

The build is Rollup based, not the webpack the introduction mentions, and the package.json scripts show the current shape. The build script removes dist, runs rollup with the config, then runs build:prepublish which executes SetupPackage.cjs, then copies the dist output. The release build sets NODE_ENV to production and does the same thing. The start script runs the prepublish step and rollup in watch mode.

That SetupPackage.cjs step is worth noting, because package.json in the repository carries a `private: true` flag while the package is still published to npm, and the publish script is standard-version with the changelog generated from conventional commits. The prepublish script is what turns the private source manifest into the publishable one, which explains why the published package and the repository manifest differ.

There is a licensing inconsistency to check before you adopt this. The package.json declares `"license": "MIT"` and the LICENSE file sits at the repository root, while the repository itself is recorded under the Apache-2.0 licence. Both are permissive, so the practical obligations are minimal either way, but the two records disagree and you should read the LICENSE file rather than rely on either label.

For the browser case there is a hosted demo linked from the README, and the examples directory is where you look for the endpoint-level usage patterns. The generated API reference is the other place, produced by typedoc into docs/.

Where mailgun.js is the wrong choice

Three situations argue against this SDK.

The first is a browser-only product. The README's own note about cross-origin limits and about not publishing your private API key means you would be running a proxy of your own, which is a server you now maintain, with the key on the other side of it. At that point you are running the Node SDK on your server and calling it from the browser, which means the SDK's browser bundles are not what you needed.

The second is a project that sends email from many accounts with per-tenant routing rules. The subaccount feature exists, but the README points to the Mailgun API documentation for how it works rather than documenting it here, so the number of Mailgun-specific behaviours you will have to read up on outside the SDK is larger than it looks.

The third is a language mismatch. This is a JavaScript SDK, so a Go or Python service will not use it, and the alternative in that direction is the raw Mailgun API over HTTP, which is what the SDK wraps anyway.

The genuine alternative to the SDK is calling the REST API directly with fetch in Node, or with your language's HTTP client. The difference in approach is that the SDK gives you typed request and response objects, four prebuilt bundles, the FormData handling, the proxy and Fetch configuration switches, and a maintained path through Mailgun API changes. Calling the API directly gives you no bundle resolution to reason about and no type layer, but also no dependency to update when a 14.x release changes a default.

The deciding factor is usually whether you are sending mail from TypeScript code with a typed domain model, in which case the definitions submodule does real work, or from a script, in which case it does not.

Editorial conclusion

mailgun.js fits a Node service that sends transactional mail through Mailgun and wants the request shapes and the response types described in TypeScript rather than assembled by hand. It does not fit a browser-only application, because the README states a proxy is required for cross-origin reasons and that publishing your private API key in frontend code is unsafe, so the browser bundle is for local demos rather than production. Verify first which bundle your build tooling resolves, since four are conditionally exported by environment, and check the licence, because package.json declares MIT while the repository is recorded under Apache-2.0.

Frequently asked questions

How do I install mailgun.js?

Run `npm install mailgun.js`. The SDK requires node.js >= 18.x, and the README links to the Mailgun API documentation for the API reference itself.

How do I create a Mailgun client in JavaScript?

Import the module, construct it with a FormData implementation, then call `mailgun.client({username: 'api', key: ...})`. The username is `api` and the key is your Mailgun API key, and you also need to pass `url: 'https://api.eu.mailgun.net'` if you use the EU infrastructure.

What does the useFetch option in mailgun.js do?

Available since version 12.1.0, it makes the SDK use the Fetch API instead of XMLHttpRequest, for environments that do not support the latter. Proxy configuration and the form-data package are not supported when useFetch is enabled.

Can I use mailgun.js directly in a browser?

The README states that a proxy is required to talk to the Mailgun API from the browser because of cross-origin limitations, and that you must not publish your private API key in frontend code. There are browser bundles for AMD and ESM, and the package ships four bundles in total, conditionally exported by environment.

How do I import TypeScript types from mailgun.js?

From the definitions submodule: `import { MailgunClientOptions, MessagesSendResult } from 'mailgun.js/definitions'`, or the namespaced form `import { Interfaces, Enums } from 'mailgun.js/definitions'` for interfaces and enum values.

Official sources

  1. License: Apache-2.0
  2. mailgun/mailgun.js on GitHub
  3. Project website
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/mailgun-mailgun-js.svg)](https://hysenlabs.com/projects/mailgun-mailgun-js)

Was this useful?