MailerSend

Anymail integrates Django with the MailerSend transactional email service, using their email API endpoint.

Settings

EMAIL_BACKEND

To use Anymail’s MailerSend backend, set:

EMAIL_BACKEND = "anymail.backends.mailersend.EmailBackend"

in your settings.py.

MAILERSEND_API_TOKEN

Required for sending. A MailerSend API token generated in your MailerSend Email domains settings. For the token permission level, “custom access” is recommended, with full access to email and no access for all other features.

ANYMAIL = {
    ...
    "MAILERSEND_API_TOKEN": "<your API token>",
}

Anymail will also look for MAILERSEND_API_TOKEN at the root of the settings file if neither ANYMAIL["MAILERSEND_API_TOKEN"] nor ANYMAIL_MAILERSEND_API_TOKEN is set.

If your Django project sends email from multiple MailerSend domains, you will need a separate API token for each domain. Use the token matching your DEFAULT_FROM_EMAIL domain in settings.py, and then override where necessary for individual emails by setting "api_token" in the message’s esp_extra. (You could centralize this logic using Anymail’s Pre-send signal.)

MAILERSEND_BATCH_SEND_MODE

If you are using Anymail’s merge_data with multiple recipients (“batch sending”), set this to indicate how to handle the batch. See Batch send mode below for more information.

Choices are "use-bulk-email" or "expose-to-list". The default None will raise an error if merge_data is used with more than one to recipient.

ANYMAIL = {
    ...
    "MAILERSEND_BATCH_SEND_MODE": "use-bulk-email",
}

MAILERSEND_SIGNING_SECRET

The MailerSend webhook signing secret needed to verify webhook posts. Required if you are using activity tracking, otherwise not necessary. (This is separate from Anymail’s WEBHOOK_SECRET setting.)

Find this in your MailerSend Email domains settings: after adding a webhook, look for the “signing secret” on the webhook’s management page.

ANYMAIL = {
    ...
    "MAILERSEND_SIGNING_SECRET": "<secret from webhook management page>",
}

MailerSend generates a unique secret for each webhook; if you edit your webhook you will need to update this setting with the new signing secret. (Also, inbound routes use a different secret, with a different setting—see below.)

MAILERSEND_INBOUND_SECRET

The MailerSend inbound route secret needed to verify inbound notifications. Required if you are using inbound routing, otherwise not necessary.

Find this in your MailerSend Email domains settings: after you have added an inbound route, look for the “secret” immediately below the route url on the management page for that inbound route.

ANYMAIL = {
    ...
    "MAILERSEND_INBOUND_SECRET": "<secret from inbound management page>",
}

MailerSend generates a unique secret for each inbound route url; if you edit your route you will need to update this setting with the new secret. (Also, activity tracking webhooks use a different secret, with a different setting—see above.)

MAILERSEND_API_URL

The base url for calling the MailerSend API.

The default is MAILERSEND_API_URL = "https://api.mailersend.com/v1/". (It’s unlikely you would need to change this.)

exp_extra support

Anymail’s MailerSend backend will pass esp_extra values directly to MailerSend’s email API.

In addition, you can override the MAILERSEND_API_TOKEN for an individual message by providing "api-token", and MAILERSEND_BATCH_SEND_MODE by providing "batch-send-mode" in the esp_extra dict.

Example:

message = AnymailMessage(...)
message.esp_extra = {
    # override your MailerSend domain's content tracking default:
    "settings": {"track_content": False},

    # use a different MAILERSEND_API_TOKEN for this message:
    "api_token": MAILERSEND_API_TOKEN_FOR_MARKETING_DOMAIN,

    # override the MAILERSEND_BATCH_SEND_MODE setting
    # just for this message:
    "batch_send_mode": "use-bulk-email",
}

Nested values are merged deeply. When sending using MailerSend’s bulk-email API endpoint, the esp_extra params are merged into the payload for every individual message in the batch.

Limitations and quirks

MailerSend does not support a few features offered by some other ESPs.

Anymail normally raises an AnymailUnsupportedFeature error when you try to send a message using features that MailerSend doesn’t support You can tell Anymail to suppress these errors and send the messages anyway – see Unsupported features.

Attachments require filenames, ignore content type

MailerSend requires every attachment (even inline ones) to have a filename. And it determines the content type of the attachment from the filename extension.

If you try to send an attachment without a filename, Anymail will substitute “attachment*.ext*” using an appropriate .ext for the content type.

If you try to send an attachment whose content type doesn’t match its filename extension, MailerSend will change the content type to match the extension. (E.g., the filename “data.txt” will always be sent as “text/plain”, even if you specified a “text/csv” content type.)

Single Reply-To

MailerSend only supports a single Reply-To address.

If your message has multiple reply addresses, you’ll get an AnymailUnsupportedFeature error—or if you’ve enabled ANYMAIL_IGNORE_UNSUPPORTED_FEATURES, Anymail will use only the first one.

Limited extra headers

MailerSend does not allow most extra headers. There are two exceptions:

  • You can include In-Reply-To in extra headers, set to a message-id (without the angle brackets).

  • You can include Precedence in extra headers to override the “Add precedence bulk header” option from your MailerSend domain advanced settings (look under “More settings”). Anymail will set MailerSend’s precedence_bulk param to true if your extra headers have Precedence set to "bulk" or "list" or "junk", or false for any other value.

Any other extra headers will raise an AnymailUnsupportedFeature error.

No metadata support

MailerSend does not support Anymail’s metadata or merge_metadata features.

No envelope sender overrides

MailerSend does not support overriding envelope_sender on individual messages. (To use a MailerSend sender identity, set the verified identity’s email address as the message’s from_email.)

API rate limits

MailerSend provides rate limit headers with each API call response. To access them after a successful send, use (e.g.,) message.anymail_status.esp_response.headers["x-ratelimit-remaining"].

If you exceed a rate limit, you’ll get an AnymailAPIError with error.status_code == 429, and can determine how many seconds to wait from error.response.headers["retry-after"].

Batch sending/merge and ESP templates

MailerSend supports ESP stored templates, on-the-fly templating, and batch sending with per-recipient merge data. MailerSend’s approaches to batch sending don’t align perfectly with Anymail’s; be sure to read Batch send mode below to understand the options.

MailerSend offers two different syntaxes for substituting data into templates: “simple personalization” and “advanced personalization.” Anymail supports only the more flexible advanced personalization syntax. If you have MailerSend templates using the “simple” syntax ({$variable_name}), you’ll need to convert them to the “advanced” syntax ({{ variable_name }}) for use with Anymail’s merge_data and merge_global_data.

Here’s an example defining an on-the-fly template that uses MailerSend advanced personalization variables:

message = EmailMessage(
    from_email="[email protected]",
    subject="Your order {{ order_no }} has shipped",
    body="""Hi {{ name }},
            We shipped your order {{ order_no }}
            on {{ ship_date }}.""",
    to=["[email protected]", "Bob <[email protected]>"]
)
# (you'd probably also set a similar html body with variables)
message.merge_data = {
    "[email protected]": {"name": "Alice", "order_no": "12345"},
    "[email protected]": {"name": "Bob", "order_no": "54321"},
}
message.merge_global_data = {
    "ship_date": "May 15"  # Anymail maps globals to all recipients
}
# (see discussion of batch-send-mode below)
message.esp_extra = {
    "batch-send-mode": "use-bulk-email"
}

To send the same message with a MailerSend stored template from your account, set template_id, and omit any plain-text or html body. If you’ve set a subject in your MailerSend template’s default settings, you can omit subject (otherwise you must include it). And if your template default settings specify the From email, that will override from_email. Example:

message = EmailMessage(
    from_email="[email protected]",
    # (subject and body from template)
    to=["[email protected]", "Bob <[email protected]>"]
)
message.template_id = "vzq12345678"  # id of template in our account
# ... set merge_data and merge_global_data as above

MailerSend does not natively support global merge data. Anymail emulates the capability by copying any merge_global_data values to every recipient.

Batch send mode

Anymail’s model for batch sending is that each recipient receives a separate email personalized for them, and that each recipient sees only their own email address in the message’s To header.

MailerSend has a bulk-email API that matches Anymail’s batch sending model, but operates completely asynchronously, which can complicate status tracking and error handling.

MailerSend also supports batch sending personalized emails through its regular email API, which avoids the bulk-email limitations but exposes the entire To list to all recipients.

If you want to use Anymail’s merge_data for batch sending to multiple to recipients, you must select one of these two approaches by specifying either "use-bulk-email" or "expose-to-list" in your Anymail MAILERSEND_BATCH_SEND_MODE setting—or as "batch-send-mode" in the message’s esp_extra.

Caution

Using the "expose-to-list" MailerSend batch send mode will reveal all of the message’s To email addresses to every recipient of the message.

If you use the "use-bulk-email" MailerSend batch send mode:

  • The message.anymail_status.status will be {"unknown"}, because MailerSend detects errors and rejected recipients at a later time.

  • The message.anymail_status.message_id will be a MailerSend bulk_email_id, prefixed with "bulk:" to distinguish it from a regular message_id.

  • You will need to poll MailerSend’s bulk-email status API to determine whether the send was successful, partially successful, or failed, and to determine the event.message_id that will be sent to status tracking webhooks.

  • Be aware that rate limits for the bulk-email API are significantly lower than MailerSend’s regular email API.

Rather than one of these batch sending options, an often-simpler approach is to loop over your recipient list and send a separate message for each. You can still use templates and merge_data:

# How to "manually" send a batch of emails to one recipient at a time.
# (There's no need to specify a MailerSend "batch-send-mode".)
to_list = ["[email protected]", "[email protected]"]
merge_data = {
    "[email protected]": {"name": "Alice", "order_no": "12345"},
    "[email protected]": {"name": "Bob", "order_no": "54321"},
}
merge_global_data = {
    "ship_date": "May 15",
}
for to_email in to_list:
    message = AnymailMessage(
        # just one recipient per message:
        to=[to_email],
        # provide template variables for this one recipient:
        merge_global_data = merge_global_data | merge_data[to_email],
        # any other attributes you want:
        template_id = "vzq12345678",
        from_email="[email protected]",
    )
    try:
        message.send()
    except AnymailAPIError:
        # Handle error -- e.g., schedule for retry later.
    else:
        # Either successful send or to_email is rejected.
        # message.anymail_status will be {"queued"} or {"rejected"}.
        # message.anymail_status.message_id can be stored to match
        # with event.message_id in a status tracking signal receiver.

Status tracking webhooks

If you are using Anymail’s normalized status tracking, follow MailerSend’s instructions to add a webhook to your domain.

  • Enter this Anymail tracking URL as the webhook’s “Endpoint URL” (where yoursite.example.com is your Django site):

    https://yoursite.example.com/anymail/mailersend/tracking/

    Because MailerSend implements webhook signing, it’s not necessary to use Anymail’s shared webhook secret for security with MailerSend webhooks. However, it doesn’t hurt to use both. If you have set an Anymail WEBHOOK_SECRET, include that random:random shared secret in the webhook endpoint URL:

https://random:random@yoursite.example.com/anymail/mailersend/tracking/

  • For “Events to send”, select any or all events you want to track.

  • After you have saved the webhook, go back into MailerSend’s webhook management page, and reveal and copy the MailerSend “webhook signing secret”. Provide that in your settings.py ANYMAIL settings as MAILERSEND_SIGNING_SECRET so that Anymail can verify calls to the webhook:

    ANYMAIL = {
        # ...
        MAILERSEND_SIGNING_SECRET = "<secret you copied>"
    }
    

For troubleshooting, MailerSend provides a helpful log of calls to the webhook. See “About webhook attempts” in their documentation for more details.

Note

MailerSend has a relatively short three second timeout for webhook calls. Be sure to avoid any lengthy operations in your Anymail tracking signal receiver function, or MailerSend will consider the notification failed at retry it. The event’s event_id field can help identify duplicate notifications.

MailerSend retries webhook notifications only twice, with delays of 10 and then 100 seconds. If your webhook is ever offline for more than a couple minutes, you many miss some tracking events. You can use MailerSend’s activity API to query for events that may have been missed.

MailerSend will report these Anymail event_types: sent, delivered, bounced, complained, unsubscribed, opened, and clicked.

The event’s esp_event field will be the complete parsed MailerSend webhook payload, including an additional wrapper object not shown in their documentation. The activity data in MailerSend’s webhook payload example is available as event.esp_event["data"].

Inbound routing

If you want to receive email from MailerSend through Anymail’s normalized inbound handling, follow MailerSend’s guide to How to set up an inbound route.

  • For “Route to” (in their step 8), enter this Anymail inbound route endpoint URL (where yoursite.example.com is your Django site):

    https://yoursite.example.com/anymail/mailersend/inbound/

    Because MailerSend signs its inbound notifications, it’s not necessary to use Anymail’s shared webhook secret for security with MailerSend inbound routing. However, it doesn’t hurt to use both. If you have set an Anymail WEBHOOK_SECRET, include that random:random shared secret in the inbound route endpoint URL:

    https://random:random@yoursite.example.com/anymail/mailersend/inbound/

  • After you have saved the inbound route, go back into MailerSend’s inbound route management page, and copy the “Secret” displayed immediately below the “Route to” URL. Provide that in your settings.py ANYMAIL settings as MAILERSEND_INBOUND_SECRET so that Anymail can verify calls to the inbound endpoint:

    ANYMAIL = {
        # ...
        MAILERSEND_INBOUND_SECRET = "<secret you copied>"
    }
    

    Note that this is a different secret from the MAILERSEND_SIGNING_SECRET used to verify activity tracking webhooks. If you are using both features, be sure to include both settings.

For troubleshooting, MailerSend provides a helpful inbound activity log near the end of the route management page. See Where to find inbound emails in their docs for more details.

Note

MailerSend imposes a three second limit on all notifications. If your inbound signal receiver function takes too long, MailerSend may think the notification failed. To avoid problems, it’s essential you offload any lengthy operations to a background task.

MailerSend does not retry failed inbound notifications. If your Django app is ever unreachable for any reason, you will miss inbound mail that arrives during that time.