Brevo
Anymail integrates with the Brevo email service (formerly Sendinblue), using their API v3. Brevo’s transactional API does not support some basic email features, such as inline images. Be sure to review the limitations below.
Changed in version 10.3: SendinBlue rebranded as Brevo in May, 2023. Anymail 10.3 uses the new name throughout its code; earlier versions used the old name. Code that refers to “SendinBlue” should continue to work, but is now deprecated. See Updating code from SendinBlue to Brevo for details.
Important
Troubleshooting: If your Brevo messages aren’t being delivered as expected, be sure to look for events in your Brevo logs.
Brevo detects certain types of errors only after the send API call reports the message as “queued.” These errors appear in the logging dashboard.
Settings
EMAIL_BACKEND
To use Anymail’s Brevo backend, set:
EMAIL_BACKEND = "anymail.backends.brevo.EmailBackend"
in your settings.py.
BREVO_API_KEY
The API key can be retrieved from your Brevo SMTP & API settings on the “API Keys” tab (don’t try to use an SMTP key). Required.
Make sure the version column indicates “v3.” (v2 keys don’t work with Anymail. If you don’t see a v3 key listed, use “Create a New API Key”.)
ANYMAIL = { ... "BREVO_API_KEY": "<your v3 API key>", }
Anymail will also look for BREVO_API_KEY at the
root of the settings file if neither ANYMAIL["BREVO_API_KEY"]
nor ANYMAIL_BREVO_API_KEY is set.
BREVO_API_URL
The base url for calling the Brevo API.
The default is BREVO_API_URL = "https://api.brevo.com/v3/"
(It’s unlikely you would need to change this.)
Changed in version 10.1: Earlier Anymail releases used https://api.sendinblue.com/v3/.
esp_extra support
To use Brevo features not directly supported by Anymail, you can
set a message’s esp_extra to
a dict that will be merged into the json sent to Brevo’s
smtp/email API.
For example, you could set Brevo’s batchId for use with their batched scheduled sending:
message.esp_extra = { 'batchId': '275d3289-d5cb-4768-9460-a990054b6c81', # merged into send params }
(You can also set "esp_extra" in Anymail’s global send defaults
to apply it to all messages.)
Limitations and quirks
Brevo’s v3 API has several limitations. In most cases below,
Anymail will raise an AnymailUnsupportedFeature
error if you try to send a message using missing features. You can
override this by enabling the ANYMAIL_IGNORE_UNSUPPORTED_FEATURES
setting, and Anymail will try to limit the API request to features
Brevo can handle.
- HTML body required
Brevo’s API returns an error if you attempt to send a message with only a plain-text body. Be sure to include HTML content for your messages if you are not using a template.
(Brevo does allow HTML without a plain-text body. This is generally not recommended, though, as some email systems treat HTML-only content as a spam signal.)
- Inline images
Brevo’s v3 API doesn’t support inline images, at all. (Confirmed with Brevo support Feb 2018.)
If you are ignoring unsupported features, Anymail will try to send inline images as ordinary image attachments.
- Attachment names must be filenames with recognized extensions
Brevo determines attachment content type by assuming the attachment’s name is a filename, and examining that filename’s extension (e.g., “.jpg”).
Trying to send an attachment without a name, or where the name does not end in a supported filename extension, will result in a Brevo API error. Anymail has no way to communicate an attachment’s desired content-type to the Brevo API if the name is not set correctly.
- Non-ASCII attachment filenames will be garbled
Brevo’s API does not properly encode Unicode characters in attachment filenames. Some email clients will display those characters incorrectly. The only workaround is to limit attachment filenames to ASCII when sending through Brevo.
- Single Reply-To
Brevo’s v3 API only supports a single Reply-To address.
If you are ignoring unsupported features and have multiple reply addresses, Anymail will use only the first one.
- Metadata exposed in message headers
Anymail passes
metadatato Brevo as a JSON-encoded string using their X-Mailin-custom email header. This header is included in the sent message, so metadata will be visible to message recipients if they view the raw message source.- Metadata and extra_headers values must be ASCII
Brevo’s API incorrectly handles non-ASCII Unicode characters in custom email headers, sending them as raw utf-8. Unencoded 8-bit values in an email header are usually invalid, and can cause bounces or silently undelivered messages.
Anymail is not able to work around this problem, so will raise an
AnymailUnsupportedFeatureerror for non-ASCIIextra_headersormerge_headersvalues.Because Anymail’s metadata uses Brevo’s X-Mailin-custom header, this also affects
metadataandmerge_metadata.Changed in version 14.0: Earlier releases did not detect this situation and could send undeliverable messages with non-ASCII headers or metadata.
- Avoid mixing non-ASCII characters and punctuation in Reply-To
Brevo’s API incorrectly handles non-ASCII characters combined with commas and certain other punctuation in address header display names.
Anymail is able to work around the problem in
to,ccandbccnames. It applies the same workaround toreply_tonames, but this triggers another Brevo bug that may cause some recipients to see an RFC 2047 encoded-word (=?utf-8?...) in their email client .Changed in version 14.0: Earlier releases did not include the workaround, resulting in Brevo sending messages with potentially missing display names in
to,ccandbccand possibly undeliverable messages forreply_to.- Special headers
Brevo uses special email headers to control certain features. You can set these using Django’s
EmailMessage.headers:message = EmailMessage( ..., headers = { "sender.ip": "10.10.1.150", # use a dedicated IP "idempotencyKey": "...uuid...", # batch send deduplication } ) # Note the constructor param is called `headers`, but the # corresponding attribute is named `extra_headers`: message.extra_headers = { "sender.ip": "10.10.1.222", "idempotencyKey": "...uuid...", }
- Delayed sending
Added in version 9.0: Earlier versions of Anymail did not support
send_atwith Brevo.- No click-tracking or open-tracking options
Brevo does not provide a way to control open or click tracking for individual messages. Anymail’s
track_clicksandtrack_openssettings are unsupported.- No envelope sender overrides
Brevo does not support overriding
envelope_senderon individual messages.- Non-ASCII mailboxes (EAI)
Brevo partially supports sending from or to Unicode mailboxes (the user part of user@domain—see EAI). Messages are delivered correctly, but in the
tofield any display name with an EAI address will be missing from the delivered message. And if any EAI address appears in theccfield, the entire Cc header will be omitted, making it effectively abcc.Also, Brevo does not properly verify the receiving SMTP server supports EAI (smtputf8). For valid EAI recipient addresses, this generally shouldn’t cause problems. For an EAI
from_emailorreply_tothis could result in lost or undeliverable messages.
Batch sending/merge and ESP templates
Changed in version 10.3: Added support for batch sending with merge_data
and merge_metadata.
Brevo supports ESP stored templates and batch sending with per-recipient merge data.
To use a Brevo template, set the message’s
template_id to the numeric
Brevo template ID, and supply substitution params using Anymail’s normalized
merge_data and
merge_global_data message attributes:
message = EmailMessage( # (subject and body come from the template, so don't include those) to=["[email protected]", "Bob <[email protected]>"] ) message.template_id = 3 # use this Brevo template message.from_email = None # to use the template's default sender 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", }
Within your Brevo template body and subject, you can refer to merge
variables using Django-like template syntax, like {{ params.order_no }} or
{{ params.ship_date }} for the example above. See Brevo’s guide to the
Brevo Template Language.
The message’s from_email (which defaults to
your DEFAULT_FROM_EMAIL setting) will override the template’s default sender.
If you want to use the template’s sender, be sure to set from_email to None
after creating the message, as shown in the example above.
You can also override the template’s subject and reply-to address (but not body)
using standard EmailMessage attributes.
Brevo also supports batch-sending without using an ESP-stored template. In this
case, each recipient will receive the same content (Brevo doesn’t support inline
templates) but will see only their own To email address. Setting either of
merge_data or
merge_metadata—even to an empty
dict—will cause Anymail to use Brevo’s batch send option ("messageVersions").
You can use Anymail’s
merge_metadata to supply custom tracking
data for each recipient:
message = EmailMessage( to=["[email protected]", "Bob <[email protected]>"], from_email="...", subject="...", body="..." ) message.merge_metadata = { '[email protected]': {'user_id': "12345"}, '[email protected]': {'user_id': "54321"}, }
To use Brevo’s “idempotencyKey” with a batch send, set it in the
message’s headers: message.extra_headers = {"idempotencyKey": "...uuid..."}.
Caution
“Old template language” not supported
Brevo once supported two different template styles: a “new” template
language that uses Django-like template syntax (with {{ param.NAME }}
substitutions), and an “old” template language that used percent-delimited
%NAME% substitutions.
Anymail 7.0 and later work only with new style templates, now known as the “Brevo Template Language.”
Although unconverted old templates may appear to work with Anymail, there can be
subtle bugs. In particular, reply_to overrides and recipient display names
are silently ignored when old style templates are sent with Anymail 7.0 or later.
If you still have old style templates, follow Brevo’s instructions to
convert each old template to the new language.
Changed in version 7.0: Dropped support for Sendinblue old template language
Status tracking webhooks
If you are using Anymail’s normalized status tracking, add the url at Brevo’s site under Transactional > Email > Settings > Webhook.
The “URL to call” is:
https://random:random@yoursite.example.com/anymail/brevo/tracking/
random:random is an
ANYMAIL_WEBHOOK_SECRETshared secretyoursite.example.com is your Django site
Be sure to select the checkboxes for all the event types you want to receive. (Also make sure you are in the “Transactional” section of their site; Brevo has a separate set of “Campaign” webhooks, which don’t apply to messages sent through Anymail.)
If you are interested in tracking opens, note that Brevo has four different open event types:
“First opening”: the first time a message is opened by a particular recipient. (Brevo event type “opened”)
“Known open”: the second and subsequent opens. (Brevo event type “unique_opened”)
“Loaded by proxy”: a message’s tracking pixel is loaded by a proxy service intended to protect users’ IP addresses. See Brevo’s article on Apple’s Mail Privacy Protection for more details. As of July, 2024, Brevo seems to deliver this event only for the second and subsequent loads by the proxy service. (Brevo event type “proxy_open”)
“First open but loaded by proxy”: the first time a message’s tracking pixel is loaded by a proxy service for a particular recipient. As of July, 2024, this event has not yet been exposed in Brevo’s webhook control panel, and you must contact Brevo support to enable it. (Brevo event type “unique_proxy_opened”)
Anymail normalizes all of these to “opened.” If you need to distinguish the
specific Brevo event types, examine the raw
esp_event, e.g.:
if event.esp_event["event"] == "unique_opened": ….
Brevo will report these Anymail event_types:
queued, rejected, bounced, deferred, delivered, opened (see note above), clicked, complained,
failed, unsubscribed, subscribed (though subscribed should never occur for transactional email).
For events that occur in rapid succession, Brevo frequently delivers them out of order. For example, it’s not uncommon to receive a “delivered” event before the corresponding “queued.” Also, note that “queued” may be received even if Brevo will not actually send the message. (E.g., if a recipient is on your blocked list due to a previous bounce, you may receive “queued” followed by “rejected.”)
The event’s esp_event field will be
a dict of raw webhook data received from Brevo.
Changed in version 10.3: Older Anymail versions used a tracking webhook URL containing “sendinblue” rather than “brevo”. The old URL will still work, but is deprecated. See Updating code from SendinBlue to Brevo below.
Changed in version 11.1: Added support for Brevo’s “Complaint,” “Error” and “Loaded by proxy” events.
Inbound webhook
Added in version 10.1.
If you want to receive email from Brevo through Anymail’s normalized inbound handling, follow Brevo’s Inbound parsing webhooks guide to enable inbound service and add Anymail’s inbound webhook.
At the “Creating the webhook” step, set the "url" param to:
https://random:random@yoursite.example.com/anymail/brevo/inbound/
random:random is an
ANYMAIL_WEBHOOK_SECRETshared secretyoursite.example.com is your Django site
Brevo does not currently seem to have a dashboard for managing or monitoring inbound service. However, you can run API calls directly from their documentation by entering your API key in “Header” field above the example, and then clicking “Try It!”. The webhooks management APIs and inbound events list API can be helpful for diagnosing inbound issues.
Changed in version 10.3: Older Anymail versions used an inbound webhook URL containing “sendinblue” rather than “brevo”. The old URL will still work, but is deprecated. See Updating code from SendinBlue to Brevo below.
Updating code from SendinBlue to Brevo
SendinBlue rebranded as Brevo in May, 2023. Anymail 10.3 has switched to the new name.
If your code refers to the old “sendinblue” name
(in EMAIL_BACKEND and ANYMAIL settings, esp_name
checks, or elsewhere) you should update it to use “brevo” instead.
If you are using Anymail’s tracking or inbound webhooks, you should
also update the webhook URLs you’ve configured at Brevo.
For compatibility, code and URLs using the old name are still functional in Anymail. But they will generate deprecation warnings, and may be removed in a future release.
To update your code:
In your settings.py, update the
EMAIL_BACKENDand rename any"SENDINBLUE_..."settings to"BREVO_...":- EMAIL_BACKEND = "anymail.backends.sendinblue.EmailBackend" # old + EMAIL_BACKEND = "anymail.backends.brevo.EmailBackend" # new ANYMAIL = { ... - "SENDINBLUE_API_KEY": "<your v3 API key>", # old + "BREVO_API_KEY": "<your v3 API key>", # new # (Also change "SENDINBLUE_API_URL" to "BREVO_API_URL" if present) # If you are using Brevo-specific global send defaults, change: - "SENDINBLUE_SEND_DEFAULTS" = {...}, # old + "BREVO_SEND_DEFAULTS" = {...}, # new }
If you are using Anymail’s status tracking webhook, go to Brevo’s dashboard (under Transactional > Email > Settings > Webhook), and change the end of the URL from
.../anymail/sendinblue/tracking/to.../anymail/brevo/tracking/. (Or use the code below to automate this.)In your tracking signal receiver function, if you are examining the
esp_nameparameter, the name will change once you have updated the webhook URL. If you had been checking whetheresp_name == "SendinBlue", change that to check ifesp_name == "Brevo".If you are using Anymail’s inbound handling, update the inbound webhook URL to change
.../anymail/sendinblue/inbound/to.../anymail/brevo/inbound/. You will need to use Brevo’s webhooks API to make the change—see below.In your inbound signal receiver function, if you are examining the
esp_nameparameter, the name will change once you have updated the webhook URL. If you had been checking whetheresp_name == "SendinBlue", change that to check ifesp_name == "Brevo".
That should be everything, but to double check you may want to search your
code for any remaining references to “sendinblue” (case-insensitive).
(E.g., grep -r -i sendinblue.)
To update both the tracking and inbound webhook URLs using Brevo’s webhooks API, you could run something like this Python code:
# Update Brevo webhook URLs to replace "anymail/sendinblue" with "anymail/brevo".
import requests
BREVO_API_KEY = "<your API key>"
headers = {
"accept": "application/json",
"api-key": BREVO_API_KEY,
}
response = requests.get("https://api.brevo.com/v3/webhooks", headers=headers)
response.raise_for_status()
webhooks = response.json()
for webhook in webhooks:
if "anymail/sendinblue" in webhook["url"]:
response = requests.put(
f"https://api.brevo.com/v3/webhooks/{webhook['id']}",
headers=headers,
json={
"url": webhook["url"].replace("anymail/sendinblue", "anymail/brevo")
}
)
response.raise_for_status()