SendGrid

Anymail integrates with the SendGrid email service, using their Web API v3.

Important

Troubleshooting: If your SendGrid messages aren’t being delivered as expected, be sure to look for “drop” events in your SendGrid activity feed.

SendGrid detects certain types of errors only after the send API call appears to succeed, and reports these errors as drop events.

Settings

EMAIL_BACKEND

To use Anymail’s SendGrid backend, set:

EMAIL_BACKEND = "anymail.backends.sendgrid.EmailBackend"

in your settings.py.

SENDGRID_API_KEY

A SendGrid API key with “Mail Send” permission. (Manage API keys in your SendGrid API key settings.) Required.

ANYMAIL = {
    ...
    "SENDGRID_API_KEY": "<your API key>",
}

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

SENDGRID_GENERATE_MESSAGE_ID

Whether Anymail should generate a UUID for each message sent through SendGrid, to facilitate status tracking. The UUID is attached to the message as a SendGrid custom arg named “anymail_id” and made available as anymail_status.message_id on the sent message.

Default True. You can set to False to disable this behavior, in which case sent messages will have a message_id of None. See Message-ID quirks below.

SENDGRID_MERGE_FIELD_FORMAT

If you use merge data, set this to a str.format() formatting string that indicates how merge fields are delimited in your SendGrid templates. For example, if your templates use the -field- hyphen delimiters suggested in some SendGrid docs, you would set:

ANYMAIL = {
    ...
    "SENDGRID_MERGE_FIELD_FORMAT": "-{}-",
}

The placeholder {} will become the merge field name. If you need to include a literal brace character, double it up. (For example, Handlebars-style {{field}} delimiters would take the format string "{{{{{}}}}}".)

The default None requires you include the delimiters directly in your merge_data keys. You can also override this setting for individual messages. See the notes on SendGrid templates and merge below.

SENDGRID_API_URL

The base url for calling the SendGrid API.

The default is SENDGRID_API_URL = "https://api.sendgrid.com/v3/" (It’s unlikely you would need to change this.)

esp_extra support

To use SendGrid features not directly supported by Anymail, you can set a message’s esp_extra to a dict of parameters for SendGrid’s v3 Mail Send API. Your esp_extra dict will be deeply merged into the parameters Anymail has constructed for the send, with esp_extra having precedence in conflicts.

Example:

message.open_tracking = True
message.esp_extra = {
    "asm": {  # SendGrid subscription management
        "group_id": 1,
        "groups_to_display": [1, 2, 3],
    },
    "tracking_settings": {
        "open_tracking": {
            # Anymail will automatically set `"enable": True` here,
            # based on message.open_tracking.
            "substitution_tag": "%%OPEN_TRACKING_PIXEL%%",
        },
    },
}

(You can also set "esp_extra" in Anymail’s global send defaults to apply it to all messages.)

Limitations and quirks

Message-ID

SendGrid does not return any sort of unique id from its send API call. Knowing a sent message’s ID can be important for later queries about the message’s status.

To work around this, Anymail generates a UUID for each outgoing message, provides it to SendGrid as a custom arg named “anymail_id” and makes it available as the message’s anymail_status.message_id attribute after sending. The same UUID will be passed to Anymail’s tracking webhooks as event.message_id.

To disable attaching tracking UUIDs to sent messages, set SENDGRID_GENERATE_MESSAGE_ID to False in your Anymail settings.

Changed in version 3.0: Previously, Anymail generated a custom Message-ID header for each sent message. But SendGrid’s “smtp-id” event field does not reliably reflect this header, which complicates status tracking. (For compatibility with messages sent in earlier versions, Anymail’s webhook message_id will fall back to “smtp-id” when “anymail_id” isn’t present.)

Single Reply-To

SendGrid’s v3 API 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.

Invalid Addresses

SendGrid will accept and send just about anything as a message’s from_email. (And email protocols are actually OK with that.)

(Tested March, 2016)

No envelope sender overrides
SendGrid does not support overriding envelope_sender on individual messages.

Batch sending/merge and ESP templates

SendGrid offers both ESP stored templates and batch sending with per-recipient merge data.

You can use a SendGrid stored template by setting a message’s template_id to the template’s unique id. Alternatively, you can refer to merge fields directly in an EmailMessage’s subject and body—the message itself is used as an on-the-fly template.

In either case, supply the merge data values with Anymail’s normalized merge_data and merge_global_data message attributes.

message = EmailMessage(
    ...
    # omit subject and body (or set to None) to use template content
    to=["alice@example.com", "Bob <bob@example.com>"]
)
message.template_id = "5997fcf6-2b9f-484d-acd5-7e9a99f0dc1f"  # SendGrid id
message.merge_data = {
    'alice@example.com': {'name': "Alice", 'order_no': "12345"},
    'bob@example.com': {'name': "Bob", 'order_no': "54321"},
}
message.merge_global_data = {
    'ship_date': "May 15",
}
message.esp_extra = {
    # Tell Anymail this SendGrid template uses "-field-" to refer to merge fields.
    # (We could also just set SENDGRID_MERGE_FIELD_FORMAT in our ANYMAIL settings.)
    'merge_field_format': "-{}-"
}

SendGrid doesn’t have a pre-defined merge field syntax, so you must tell Anymail how substitution fields are delimited in your templates. There are three ways you can do this:

  • Set 'merge_field_format' in the message’s esp_extra to a python str.format() string, as shown in the example above. (This applies only to that particular EmailMessage.)
  • Or set SENDGRID_MERGE_FIELD_FORMAT in your Anymail settings. This is usually the best approach, and will apply to all messages sent through SendGrid. (You can still use esp_extra to override for individual messages.)
  • Or include the field delimiters directly in all your merge_data and merge_global_data keys. E.g.: {'-name-': "Alice", '-order_no-': "12345"}. (This can be error-prone, and difficult to move to other ESPs.)

When you supply per-recipient merge_data, Anymail automatically changes how it communicates the “to” list to SendGrid, so that so that each recipient sees only their own email address. (Anymail creates a separate “personalization” for each recipient in the “to” list; any cc’s or bcc’s will be duplicated for every to-recipient.)

SendGrid templates allow you to mix your EmailMessage’s subject and body with the template subject and body (by using <%subject%> and <%body%> in your SendGrid template definition where you want the message-specific versions to appear). If you don’t want to supply any additional subject or body content from your Django app, set those EmailMessage attributes to empty strings or None.

See the SendGrid’s template overview and transactional template docs for more information.

Status tracking webhooks

If you are using Anymail’s normalized status tracking, enter the url in your SendGrid mail settings, under “Event Notification”:

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

Be sure to check the boxes in the SendGrid settings for the event types you want to receive.

SendGrid will report these Anymail event_types: queued, rejected, bounced, deferred, delivered, opened, clicked, complained, unsubscribed, subscribed.

The event’s esp_event field will be a dict of Sendgrid event fields, for a single event. (Although SendGrid calls webhooks with batches of events, Anymail will invoke your signal receiver separately for each event in the batch.)

Inbound webhook

If you want to receive email from SendGrid through Anymail’s normalized inbound handling, follow SendGrid’s Inbound Parse Webhook guide to set up Anymail’s inbound webhook.

The Destination URL setting will be:

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

Be sure the URL has a trailing slash. (SendGrid’s inbound processing won’t follow Django’s APPEND_SLASH redirect.)

If you want to use Anymail’s normalized spam_detected and spam_score attributes, be sure to enable the “Check incoming emails for spam” checkbox.

You have a choice for SendGrid’s “POST the raw, full MIME message” checkbox. Anymail will handle either option (and you can change it at any time). Enabling raw MIME will give the most accurate representation of any received email (including complex forms like multi-message mailing list digests). But disabling it may use less memory while processing messages with many large attachments.