---
title: "Switch from Mailgun"
description: "Keep the Mailgun SDK and send through OpenEmail. Change its base URL and its key, and your sending code stays as it is."
url: "https://openemail.uk/docs/knowledge/developers/switch-from-mailgun"
area: "Knowledge base"
category: "Developers"
status: "live"
---

# Switch from Mailgun

Keep the Mailgun SDK and send through OpenEmail. 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 OpenEmail API key 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 OpenEmail.

**Point the Mailgun SDK at OpenEmail**

_Node.js_

```nodejs
import formData from 'form-data'
import Mailgun from 'mailgun.js'

const mailgun = new Mailgun(formData)
const mg = mailgun.client({
  username: 'api',
  key: process.env.OPENEMAIL_API_KEY,
  url: 'https://api.openemail.uk/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>',
})
```

_Python_

```python
import os
from mailgun.client import Client

with Client(
    auth=("api", os.environ["OPENEMAIL_API_KEY"]),
    api_url="https://api.openemail.uk/compat/mailgun",
) as client:
    client.messages.create(
        domain="acme.com",
        data={
            "from": "Acme Billing <billing@acme.com>",
            "to": ["ada@example.com"],
            "subject": "Your invoice",
            "html": "<p>Your invoice is attached.</p>",
        },
    )
```

_Ruby_

```ruby
require 'mailgun-ruby'

mg = Mailgun::Client.new(
  ENV.fetch('OPENEMAIL_API_KEY'),
  'api.openemail.uk/compat/mailgun'
)
mg.send_message('acme.com', {
  from: 'Acme Billing <billing@acme.com>',
  to: 'ada@example.com',
  subject: 'Your invoice',
  html: '<p>Your invoice is attached.</p>'
})
```

_PHP_

```php
<?php

use Http\Client\Common\Plugin\AddPathPlugin;
use Http\Client\Common\PluginClient;
use Http\Discovery\Psr17FactoryDiscovery;
use Http\Discovery\Psr18ClientDiscovery;
use Mailgun\HttpClient\HttpClientConfigurator;
use Mailgun\Mailgun;

$configurator = (new HttpClientConfigurator())
    ->setApiKey(getenv('OPENEMAIL_API_KEY'))
    ->setEndpoint('https://api.openemail.uk')
    ->setHttpClient(new PluginClient(Psr18ClientDiscovery::find(), [
        new AddPathPlugin(Psr17FactoryDiscovery::findUriFactory()->createUri('/compat/mailgun')),
    ]));

$mg = new Mailgun($configurator);
$mg->messages()->send('acme.com', [
    'from' => 'Acme Billing <billing@acme.com>',
    'to' => 'ada@example.com',
    'subject' => 'Your invoice',
    'html' => '<p>Your invoice is attached.</p>',
]);
```

_curl_

```bash
curl -s --user "api:$OPENEMAIL_API_KEY" \
  https://api.openemail.uk/compat/mailgun/v3/acme.com/messages \
  -F from='Acme Billing <billing@acme.com>' \
  -F to=ada@example.com \
  -F subject='Your invoice' \
  -F 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 OpenEmail 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 OpenEmail |
| --- | --- |
| `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 5 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 an OpenEmail template, 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 an OpenEmail template supplies the whole body, and a `t:version` that is not a version number.
- `o:deliverytime-optimize-period` and `o:time-zone-localize`, because OpenEmail 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 OpenEmail 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.
