Moving from Mailgun

Move an app built for Mailgun to Mailroomly without rewriting it: it keeps its Mailgun SDK, its webhook handler and its route handler, and you change the API URL and the key.

What stays the same

Mailroomly speaks Mailgun's messages API and its webhook formats:

  • The endpoints. POST /v3/{domain}/messages and POST /v3/{domain}/messages.mime, at the root of https://app.mailroomly.com, take Mailgun's form: from, to, cc, bcc, subject, text, html, attachment, inline, recipient-variables for batch sending, o:tag, open and click tracking, test mode, h: headers and v: variables. They answer {"id": "<…>", "message": "Queued. Thank you."}, and errors in Mailgun's {"message": "…"} shape.
  • The key. An app's API key goes where the Mailgun key was: the SDK sends it as Mailgun's, with the user api.
  • Batch sending. With recipient-variables, each recipient gets their own email, with %recipient.name% replaced by their values. A batch is checked whole before anything is sent.
  • Test mode. o:testmode=yes checks the request (its fields, addresses, sender domain and key) and sends nothing.
  • Webhooks. With the Mailgun-compatible format, events arrive as Mailgun's: accepted, delivered, failed, opened, clicked, complained and unsubscribed, with your v: variables in user-variables and the answered id in message.headers.message-id. They are signed as Mailgun signs them, so your handler's signature check keeps working with the app's signing secret.
  • Routes. Email your addresses receive arrives as a Mailgun route forwards it, with its fields and attachments.

Every field and event: Mailgun compatibility.

Before you start

  • A verified domain. The app's sending domain must be verified in Mailroomly (Your own domain). Mailroomly's DNS records and Mailgun's sit side by side, so both can send for the same domain while you move. Mailroomly sends from the From address: the domain in the request's path stays yours, and the From address's domain is the one that must be verified.
  • A daily limit that fits. A new account sends to a limited number of recipients per day until it becomes a trusted account. Check Plan before moving a busy app, and ask for a higher limit if you need one (Sending trust).
  • Your blocked addresses. Mailgun's suppressions (bounces, complaints, unsubscribes) don't move by themselves. Export them from Mailgun and block them in Activity › Blocked › Block an address, or through the API, before the first send (Blocked). Block bounces and complaints for every type of email, and unsubscribes for marketing.
  • What Mailroomly doesn't do. Stored templates (template, t: options), scheduled sends (o:deliverytime), AMP, sending IPs and o:archive-to are refused with the reason, never ignored: render the content in the app, and send when the email should go.

Move an app over

Apps move one at a time. Apps › Move over from Mailgun (also in Ctrl K) walks you through the three steps in the web app: connect an app in Mailgun's format, point Mailgun's SDK at Mailroomly, and move your webhooks.

1. Create the app and its key

In Move over from Mailgun, choose Connect an app under Connect an app in Mailgun’s format. Give the App name and choose What it does (Sends email is enough for most apps), then Which emails does this key reach?, then Create the app and its key. The app's webhooks already use the Mailgun-compatible format.

The key, starting with mp_live_, takes the place of your Mailgun API key. It is shown only once: copy it into the app's secret store. To narrow it to the app's domains or to one type of email, create another key on the app's page and revoke the first (Apps and keys).

2. Point Mailgun's SDK at Mailroomly

Change two values in the app's configuration: the API URL and the key. The code that builds and sends the email stays the same.

// Node
const Mailgun = require('mailgun.js')
const FormData = require('form-data')

const mg = new Mailgun(FormData).client({
  username: 'api',
  key: process.env.MAILROOMLY_API_KEY,
  url: 'https://app.mailroomly.com',
})
// PHP
$mg = Mailgun::create(getenv('MAILROOMLY_API_KEY'), 'https://app.mailroomly.com');

Other SDKs have the same setting, the API base URL, where https://api.mailgun.net (or https://api.eu.mailgun.net) was. Without an SDK, send the same request to https://app.mailroomly.com/v3/{domain}/messages, with the key as the password of the user api.

Email goes as transactional email. To send marketing email, use a key restricted to marketing: the recipients can then unsubscribe, and %unsubscribe_url% becomes each recipient's own unsubscribe link (Apps and keys).

3. Move your webhooks

On the app's page, open the Webhook step:

  1. Webhook URL: the endpoint that received Mailgun's webhooks. Mailgun sets one URL per event; here one URL receives every event the app chooses, so pick the endpoint whose handler reads the event field, or give each handler its own app.
  2. Payload format: Mailgun-compatible (already set by the guide).
  3. Events to send: tick what the endpoint handles: Accepted for sending, Not sent (invalid or blocked address), Delivered, Bounced, Delivery delayed, Opened, Clicked, Complaint, Unsubscribed and Inbound message. A bounce, a delay and a recipient left out each arrive as Mailgun's failed event.
  4. Save changes, then Send a test event to see what your endpoint answers.
  5. Under Advanced details, choose Reveal secret and copy the Signing secret (to verify the webhooks you receive) into your webhook handler, where Mailgun's webhook signing key was.

Rotate secret replaces the secret at once: your handler can't verify webhooks until it has the new one, so put it there right after rotating. Deliveries it refuses meanwhile are retried.

Received email comes as a form (multipart/form-data with attachments, application/x-www-form-urlencoded without, as Mailgun sends it), events as JSON, so one handler can tell them apart. If your webhook handler and your route handler sit at two URLs, give the receiving address its own Inbound webhook URL (step 4) and untick Inbound message here, so received email reaches only that URL. The URL can't carry a user name and password: check the signature instead (Webhooks).

4. Receive email, if the app did with Mailgun

For an app that received email through a Mailgun route that forwarded it to a URL:

  1. Set the domain to Sends and receives (What this domain does, in its Settings), usually the same subdomain the route matched, never a domain whose mail lives elsewhere (Addresses and domains).
  2. Create the receiving address and choose the app as its Owning app. Give it an Inbound webhook URL if your route handler has its own URL. Requests arrive as the route forwarded them, signed as Mailgun signs them, with the app's signing secret.
  3. If the route matched any address of the subdomain, make the address the domain's Catch-all address: the address each email was sent to is in its recipient field.
  4. Change the domain's MX records to the value on the domain's page (DNS records), and wait until the page shows it in place. Lower the records' time to live a day ahead, so you can switch back quickly.

Setting an address's owning app or inbound webhook URL needs a person signed in to Mailroomly: an agent can't do it (AI agents).

5. Test

Send one email from the app with o:testmode=yes: the answer is Queued. Thank you. and nothing is sent. Then send one for real: the email shows in Activity, going from Sent to Delivered. Send a test as this app, on the app's page, sends one to you and lists the events your webhook received (Send a test email). For receiving, send an email to the address: it appears in the inbox, and Webhook deliveries shows the delivery to your endpoint.

6. Watch Activity

For the first days, keep an eye on:

  • Activity: every send, with its route and events (A send's page). Blocked shows the addresses Mailroomly doesn't send to, and why.
  • Webhook deliveries: deliveries your endpoint did not accept, to fix and replay.
  • Notices: bounces, complaints, daily limits and failed webhooks, each saying what to do.

Switching back

Keep the Mailgun account, its DNS records, its webhooks and its routes until the move is done. To go back, restore the two values, the API URL and the key: the app sends through Mailgun again within minutes. For receiving, point the MX records back to Mailgun. Email already accepted by Mailroomly is still sent, and stays in Activity.

What differs

In Mailroomly
Templates Stored templates are refused: render the content in the app before sending.
Mailgun's other APIs Domains, mailing lists, suppressions, events and logs, statistics, tags, routes, webhook settings, validation and stored messages are not emulated. Use the REST API and the web app (Activity, Blocked, Addresses, Campaigns).
The domain in the path Names the request; the email goes from its From address, whose domain must be verified.
Each recipient of a batch Is one email in Mailroomly, with its own page in Activity. They share the call's id.
Marketing email Sent with a key restricted to marketing. An unsubscribe covers all of your organization's marketing, not one tag or list.
Ids The answered id is a UUID at your sending domain.
Blocked recipients Left out of the email, as Mailgun drops them, and reported as a failed event.
Webhooks One URL per app for every event, rather than one per event.
Limits 500 recipients per batch, 50 recipients and 10 MB per email, 15 MB per request, 100 requests per second per key. An attachment every recipient of a batch shares counts once for each: a batch's emails come to at most 50 MB.
Refusals A parameter Mailroomly doesn't support is 400 with its name; an unverified domain, a key outside its scope or marketing that isn't allowed is 403; the daily limit, or the key's own, is 429 until midnight UTC, and so is a plan's month of emails used up; a bad or missing key is 401.
Event fields No storage, envelope, geolocation, ip or campaigns.
Route fields No stripped-html, stripped-signature or spam score.
Webhook retries Failed deliveries are retried for about 45 hours, then marked Dead and can be replayed (Webhooks). As with Mailgun, an answer of 406 refuses a delivery for good: it is marked Dead at once, with no retry and no alert. Each attempt must finish within 10 seconds: answer 2xx at once and do the work afterward.

The developer reference, field by field: Mailgun compatibility.