Batch sending/merge and ESP templates

If your ESP offers templates and batch-sending/merge capabilities, Anymail can simplify using them in a portable way. Anymail doesn’t translate template syntax between ESPs, but it does normalize using templates and providing merge data for batch sends.

Here’s an example using both an ESP stored template and merge data:

from django.core.mail import EmailMessage

message = EmailMessage(
    subject=None,  # use the subject in our stored template
    to=["Wile E. <>", ""])
message.template_id = "after_sale_followup_offer"  # use this ESP stored template
message.merge_data = {  # per-recipient data to merge into the template
    '': {'NAME': "Wile E.",
                         'OFFER': "15% off anvils"},
    '':   {'NAME': "Mr. Runner"},
message.merge_global_data = {  # merge data for all recipients
    'PARTNER': "Acme, Inc.",
    'OFFER': "5% off any Acme product",  # a default if OFFER missing for recipient

The message’s template_id identifies a template stored at your ESP which provides the message body and subject. (Assuming the ESP supports those features.)

The message’s merge_data supplies the per-recipient data to substitute for merge fields in your template. Setting this attribute also lets Anymail know it should use the ESP’s batch sending feature to deliver separate, individually-customized messages to each address on the “to” list. (Again, assuming your ESP supports that.)


Templates and batch sending capabilities can vary widely between ESPs, as can the syntax for merge fields. Be sure to read the notes for your specific ESP, and test carefully with a small recipient list before launching a gigantic batch send.

Although related and often used together, ESP stored templates and merge data are actually independent features. For example, some ESPs will let you use merge field syntax directly in your EmailMessage body, so you can do customized batch sending without needing to define a stored template at the ESP.

ESP stored templates

Many ESPs support transactional email templates that are stored and managed within your ESP account. To use an ESP stored template with Anymail, set template_id on an EmailMessage.


The identifier of the ESP stored template you want to use. For most ESPs, this is a str name or unique id. (See the notes for your specific ESP.)

message.template_id = "after_sale_followup_offer"

With most ESPs, using a stored template will ignore any body (plain-text or HTML) from the EmailMessage object.

A few ESPs also allow you to define the message’s subject as part of the template, but any subject you set on the EmailMessage will override the template subject. To use the subject stored with the ESP template, set the message’s subject to None:

message.subject = None  # use subject from template (if supported)

Similarly, some ESPs can also specify the “from” address in the template definition. Set message.from_email = None to use the template’s “from.” (You must set this attribute after constructing an EmailMessage object; passing from_email=None to the constructor will use Django’s DEFAULT_FROM_EMAIL setting, overriding your template value.)

Batch sending with merge data

Several ESPs support “batch transactional sending,” where a single API call can send messages to multiple recipients. The message is customized for each email on the “to” list by merging per-recipient data into the body and other message fields.

To use batch sending with Anymail (for ESPs that support it):

  • Use “merge fields” (sometimes called “substitution variables” or similar) in your message. This could be in an ESP stored template referenced by template_id, or with some ESPs you can use merge fields directly in your EmailMessage (meaning the message itself is treated as an on-the-fly template).

  • Set the message’s merge_data attribute to define merge field substitutions for each recipient, and optionally set merge_global_data to defaults or values to use for all recipients.

  • Specify all of the recipients for the batch in the message’s to list.


    It’s critical to set the merge_data attribute: this is how Anymail recognizes the message as a batch send.

    When you provide merge_data, Anymail will tell the ESP to send an individual customized message to each “to” address. Without it, you may get a single message to everyone, exposing all of the email addresses to all recipients. (If you don’t have any per-recipient customizations, but still want individual messages, just set merge_data to an empty dict.)

The exact syntax for merge fields varies by ESP. It might be something like *|NAME|* or -name- or <%name%>. (Check the notes for your ESP, and remember you’ll need to change the template if you later switch ESPs.)


A dict of per-recipient template substitution/merge data. Each key in the dict is a recipient email address, and its value is a dict of merge field names and values to use for that recipient:

message.merge_data = {
    '': {'NAME': "Wile E.",
                         'OFFER': "15% off anvils"},
    '':   {'NAME': "Mr. Runner",
                         'OFFER': "instant tunnel paint"},

When merge_data is set, Anymail will use the ESP’s batch sending option, so that each to recipient gets an individual message (and doesn’t see the other emails on the to list).


A dict of template substitution/merge data to use for all recipients. Keys are merge field names in your message template:

message.merge_global_data = {
    'PARTNER': "Acme, Inc.",
    'OFFER': "5% off any Acme product",  # a default OFFER

Merge data values must be strings. (Some ESPs also allow other JSON-serializable types like lists or dicts.) See Formatting merge data for more information.

Like all Anymail additions, you can use these extended template and merge attributes with any EmailMessage or subclass object. (It doesn’t have to be an AnymailMessage.)

Tip: you can add merge_global_data to your global Anymail send defaults to supply merge data available to all batch sends (e.g, site name, contact info). The global defaults will be merged with any per-message merge_global_data.

Formatting merge data

If you’re using a date, datetime, Decimal, or anything other than strings and integers, you’ll need to format them into strings for use as merge data:

product = Product.objects.get(123)  # A Django model
total_cost = Decimal('19.99')
ship_date = date(2015, 11, 18)

# Won't work -- you'll get "not JSON serializable" errors at send time:
message.merge_global_data = {
    'PRODUCT': product,
    'TOTAL_COST': total_cost,
    'SHIP_DATE': ship_date

# Do something this instead:
message.merge_global_data = {
    'PRODUCT':,  # assuming name is a CharField
    'TOTAL_COST': "%.2f" % total_cost,
    'SHIP_DATE': ship_date.strftime('%B %d, %Y')  # US-style "March 15, 2015"

These are just examples. You’ll need to determine the best way to format your merge data as strings.

Although floats are usually allowed in merge data, you’ll generally want to format them into strings yourself to avoid surprises with floating-point precision.

Anymail will raise AnymailSerializationError if you attempt to send a message with merge data (or metadata) that can’t be sent to your ESP.

ESP templates vs. Django templates

ESP templating languages are generally proprietary, which makes them inherently non-portable.

Anymail only exposes the stored template capabilities that your ESP already offers, and then simplifies providing merge data in a portable way. It won’t translate between different ESP template syntaxes, and it can’t do a batch send if your ESP doesn’t support it.

There are two common cases where ESP template and merge features are particularly useful with Anymail:

  • When the people who develop and maintain your transactional email templates are different from the people who maintain your Django page templates. (For example, you use a single ESP for both marketing and transactional email, and your marketing team manages all the ESP email templates.)
  • When you want to use your ESP’s batch-sending capabilities for performance reasons, where a single API call can trigger individualized messages to hundreds or thousands of recipients. (For example, sending a daily batch of shipping notifications.)

If neither of these cases apply, you may find that using Django templates can be a more portable and maintainable approach for building transactional email.