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.openemail.uk/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.openemail.uk/compat/mailgun',}) await mg.messages.create('acme.com', { from: 'Acme Billing <[email protected]>', to: ['[email protected]'], 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.
| Mailgun | In Nextmail |
|---|---|
| from | The sender, with its name. |
| to | Recipients, repeated or separated by commas. With cc and bcc, up to 50 per message. |
| subject | The subject. |
| html | The HTML body. text becomes the text body, and one of the two, or template, is required. |
| attachment | Files, 20 at most and 10 MB in all. |
| inline | An 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:tag | Tags 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:deliverytime | A scheduled send, up to a year ahead. A time already past sends at once. |
| o:tracking | With o:tracking-clicks and o:tracking-opens, turns tracking on or off for the message. htmlonly counts as on. |
| o:testmode | yes records the message as sent without delivering it, as an oe_test_ key does. |
| h:Reply-To | The reply-to address. Any other h: field becomes a custom header: X-*, List-*, Precedence, Auto-Submitted, Importance, Priority and Feedback-ID. |
| recipient-variables | A 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. |
| template | The 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
templatewithhtmlortext, because a template in Nextmail supplies the whole body, and at:versionthat is not a version number. o:deliverytime-optimize-periodando:time-zone-localize, because Nextmail does not pick a send time for each recipient. Otherh:X-Mailgun-headers, which are instructions to Mailgun: use the matchingo:option instead.amp-htmlon its own. Next tohtmlortextit 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 anid: the Nextmail message id in angle brackets, whichGET /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. AnIdempotency-Keyheader 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 withDomain not found. Everything else comes back as amessage: 400 for a message that cannot be sent, 403 for a key withoutemails: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.