Skip to the documentation
Knowledge base

Switch from Mailgun

Keep the Mailgun SDK and send through Nextmail. Change its base URL and its key, and your sending code stays as it is.

What to change

Point the SDK at https://api.nextmail.sh/compat/mailgun and give it an API key from Nextmail with the emails:send permission in place of the Mailgun key. It travels as the password of the same HTTP Basic sign-in, and the user name is not checked. The domain in the path has to be one of the workspace’s domains, and the From address decides whether a message may go out, as it does everywhere in Nextmail.

import formData from 'form-data'import Mailgun from 'mailgun.js' const mailgun = new Mailgun(formData)const mg = mailgun.client({  username: 'api',  key: process.env.NEXTMAIL_API_KEY,  url: 'https://api.nextmail.sh/compat/mailgun',}) await mg.messages.create('acme.com', {  from: 'Acme Billing <billing@acme.com>',  to: ['ada@example.com'],  subject: 'Your invoice',  html: '<p>Your invoice is attached.</p>',})

In Ruby, the second argument is the host and the path without a scheme. In PHP, the SDK keeps only the host of the endpoint it is given, so the path goes in through AddPathPlugin from php-http, which the SDK already installs. The official Python package may log a warning that the host is not Mailgun’s, and sends anyway. It also retries a request that failed with 429 or a 5xx, so Nextmail answers 400 rather than 5xx once part of a batch has gone out.

What maps to what

POST /v3/{domain}/messages is the endpoint served, as multipart/form-data, which attachments need, or application/x-www-form-urlencoded. A field name ending in [] is read without it.

MailgunIn Nextmail
fromThe sender, with its name.
toRecipients, repeated or separated by commas. With cc and bcc, up to 50 per message.
subjectThe subject.
htmlThe HTML body. text becomes the text body, and one of the two, or template, is required.
attachmentFiles, 20 at most and 10 MB in all.
inlineAn image the HTML uses as cid: and its file name is embedded where it appears. Any other inline file arrives as an ordinary attachment.
o:tagTags named tag, tag_2 and so on, each holding one tag.
v:Each variable becomes a tag with its name and value. With o:tag, at most 10 per message.
o:deliverytimeA scheduled send, up to a year ahead. A time already past sends at once.
o:trackingWith o:tracking-clicks and o:tracking-opens, turns tracking on or off for the message. htmlonly counts as on.
o:testmodeyes records the message as sent without delivering it, as an oe_test_ key does.
h:Reply-ToThe reply-to address. Any other h: field becomes a custom header: X-*, List-*, Precedence, Auto-Submitted, Importance, Priority and Feedback-ID.
recipient-variablesA batch send. Every to address gets its own message, with %recipient.key% filled in from its variables and %recipient% as its address, and cc and bcc go on each one. A placeholder without a value is left as it is.
templateThe slug or id (tpl_...) of a template in Nextmail, filled in from t:variables, or else from h:X-Mailgun-Variables. t:version picks a version by its number.

What is refused, and why

  • A template with html or text, because a template in Nextmail supplies the whole body, and a t:version that is not a version number.
  • o:deliverytime-optimize-period and o:time-zone-localize, because Nextmail does not pick a send time for each recipient. Other h:X-Mailgun- headers, which are instructions to Mailgun: use the matching o: option instead.
  • amp-html on its own. Next to html or text it is left out, because those still carry the message.
  • More than one reply-to address, more than 10 tags, a tag name other than letters, digits, _ and -, and a batch of more than 100 recipients. Mailgun takes 1,000, so split larger batches.

o:dkim, o:require-tls, o:skip-verification, o:sending-ip, o:sending-ip-pool, o:tracking-pixel-location-top, o:archive-to, o:deliver-within and t:text are accepted and change nothing.

Responses and errors

  • A send answers 200 with the message Queued. Thank you. and an id: the Nextmail message id in angle brackets, which GET /emails/{id} and webhooks use without them. A batch send makes one message per recipient, each with its own id, and answers with the first. An Idempotency-Key header works as it does on the rest of the API.
  • A missing or unknown key answers 401 with the plain text Forbidden, and a domain the workspace does not have answers 404 with Domain not found. Everything else comes back as a message: 400 for a message that cannot be sent, 403 for a key without emails:send, a From address the key may not use, a domain that cannot send yet or a workspace that has used up its sending allowance, and 413 for a body over 25 MB or attachments over 5 MB.
  • When one recipient of a batch fails after others were accepted, the error names the messages already sent and answers 400, so an SDK that retries does not send them twice.

Bring your account across

Migrations → From a provider reads a Mailgun account with one API key and brings across what can leave it, from both its US and its EU region. Running it twice duplicates nothing, and the key is held encrypted only while the import runs.

  • Suppressions keep a reason: bounces as bounces, complaints as complaints, and unsubscribes that cover every tag as manual.
  • Every mailing list becomes an audience, with its subscribed members as contacts.
  • Templates arrive from the account and from each domain, with their active version.
  • Domain and account webhooks are registered with the same URL and the nearest matching events, switched off, because deliveries here are signed differently.
  • Domains come back as a checklist with their records, and API keys by name, because a signing key or a token cannot leave the service that made it.