Files
django-anymail/docs/esps/sparkpost.rst
medmunds 61660cd5ff SparkPost: call HTTP API directly [breaking]
Switch from the (now unmaintained) python-sparkpost
client library to a requests-based backend that calls
SparkPost's Transmissions API directly.

Also adds support for text/x-amp-html alternative parts
(which are supported by the SparkPost API, but weren't
by the client library).

Closes #203
2020-09-11 11:10:24 -07:00

299 lines
11 KiB
ReStructuredText

.. _sparkpost-backend:
SparkPost
=========
Anymail integrates with the `SparkPost`_ email service, using their
`Transmissions API`_.
.. versionchanged:: 8.0
Earlier Anymail versions used the official Python :pypi:`sparkpost` API client.
That library is no longer maintained, and Anymail now calls SparkPost's HTTP API
directly. This change should not affect most users, but you should make sure you
provide :setting:`SPARKPOST_API_KEY <ANYMAIL_SPARKPOST_API_KEY>` in your
Anymail settings (Anymail doesn't check environment variables), and if you are
using Anymail's :ref:`esp_extra <sparkpost-esp-extra>` you will need to update that
to use Transmissions API parameters.
.. _SparkPost: https://www.sparkpost.com/
.. _Transmissions API: https://developers.sparkpost.com/api/transmissions/
Settings
--------
.. rubric:: EMAIL_BACKEND
To use Anymail's SparkPost backend, set:
.. code-block:: python
EMAIL_BACKEND = "anymail.backends.sparkpost.EmailBackend"
in your settings.py.
.. setting:: ANYMAIL_SPARKPOST_API_KEY
.. rubric:: SPARKPOST_API_KEY
A SparkPost API key with at least the "Transmissions: Read/Write" permission.
(Manage API keys in your `SparkPost account API keys`_.)
.. code-block:: python
ANYMAIL = {
...
"SPARKPOST_API_KEY": "<your API key>",
}
Anymail will also look for ``SPARKPOST_API_KEY`` at the
root of the settings file if neither ``ANYMAIL["SPARKPOST_API_KEY"]``
nor ``ANYMAIL_SPARKPOST_API_KEY`` is set.
.. versionchanged:: 8.0
This setting is required. If you store your API key in an environment variable, load
it into your Anymail settings: ``"SPARKPOST_API_KEY": os.environ["SPARKPOST_API_KEY"]``.
(Earlier Anymail releases used the SparkPost Python library, which would look for
the environment variable.)
.. _SparkPost account API keys: https://app.sparkpost.com/account/credentials
.. setting:: ANYMAIL_SPARKPOST_API_URL
.. rubric:: SPARKPOST_API_URL
The `SparkPost API Endpoint`_ to use. The default is ``"https://api.sparkpost.com/api/v1"``.
Set this to use a SparkPost EU account, or to work with any other API endpoint including
SparkPost Enterprise API and SparkPost Labs.
.. code-block:: python
ANYMAIL = {
...
"SPARKPOST_API_URL": "https://api.eu.sparkpost.com/api/v1", # use SparkPost EU
}
You must specify the full, versioned API endpoint as shown above (not just the base_uri).
.. _SparkPost API Endpoint: https://developers.sparkpost.com/api/index.html#header-api-endpoints
.. _sparkpost-esp-extra:
esp_extra support
-----------------
To use SparkPost features not directly supported by Anymail, you can set
a message's :attr:`~anymail.message.AnymailMessage.esp_extra` to a `dict`
of `transmissions API request body`_ data. Anymail will deeply merge your overrides
into the normal API payload it has constructed, with esp_extra taking precedence
in conflicts.
Example (you probably wouldn't combine all of these options at once):
.. code-block:: python
message.esp_extra = {
"options": {
# Treat as transactional for unsubscribe and suppression:
"transactional": True,
# Override your default dedicated IP pool:
"ip_pool": "transactional_pool",
},
# Add a description:
"description": "Test-run for new templates",
"content": {
# Use draft rather than published template:
"use_draft_template": True,
# Use an A/B test:
"ab_test_id": "highlight_support_links",
},
# Use a stored recipients list (overrides message to/cc/bcc):
"recipients": {
"list_id": "design_team"
},
}
Note that including ``"recipients"`` in esp_extra will *completely* override the
recipients list Anymail generates from your message's to/cc/bcc fields, along with any
per-recipient :attr:`~anymail.message.AnymailMessage.merge_data` and
:attr:`~anymail.message.AnymailMessage.merge_metadata`.
(You can also set `"esp_extra"` in Anymail's :ref:`global send defaults <send-defaults>`
to apply it to all messages.)
.. _transmissions API request body:
https://developers.sparkpost.com/api/transmissions/#header-request-body
Limitations and quirks
----------------------
.. _sparkpost-message-id:
**Anymail's `message_id` is SparkPost's `transmission_id`**
The :attr:`~anymail.message.AnymailStatus.message_id` Anymail sets
on a message's :attr:`~anymail.message.AnymailMessage.anymail_status`
and in normalized webhook :class:`~anymail.signals.AnymailTrackingEvent`
data is actually what SparkPost calls "transmission_id".
Like Anymail's message_id for other ESPs, SparkPost's transmission_id
(together with the recipient email address), uniquely identifies a
particular message instance in tracking events.
(The transmission_id is the only unique identifier available when you
send your message. SparkPost also has something called "message_id", but
that doesn't get assigned until after the send API call has completed.)
If you are working exclusively with Anymail's normalized message status
and webhook events, the distinction won't matter: you can consistently
use Anymail's `message_id`. But if you are also working with raw webhook
esp_event data or SparkPost's events API, be sure to think "transmission_id"
wherever you're speaking to SparkPost.
**Single tag**
Anymail uses SparkPost's "campaign_id" to implement message tagging.
SparkPost only allows a single campaign_id per message. If your message has
two or more :attr:`~anymail.message.AnymailMessage.tags`, you'll get an
:exc:`~anymail.exceptions.AnymailUnsupportedFeature` error---or
if you've enabled :setting:`ANYMAIL_IGNORE_UNSUPPORTED_FEATURES`,
Anymail will use only the first tag.
(SparkPost's "recipient tags" are not available for tagging *messages*.
They're associated with individual *addresses* in stored recipient lists.)
**AMP for Email**
SparkPost supports sending AMPHTML email content. To include it, use
``message.attach_alternative("...AMPHTML content...", "text/x-amp-html")``
(and be sure to also include regular HTML and/or text bodies, too).
.. versionadded:: 8.0
**Envelope sender may use domain only**
Anymail's :attr:`~anymail.message.AnymailMessage.envelope_sender` is used to
populate SparkPost's `'return_path'` parameter. Anymail supplies the full
email address, but depending on your SparkPost configuration, SparkPost may
use only the domain portion and substitute its own encoded mailbox before
the @.
.. _sparkpost-templates:
Batch sending/merge and ESP templates
-------------------------------------
SparkPost offers both :ref:`ESP stored templates <esp-stored-templates>`
and :ref:`batch sending <batch-send>` with per-recipient merge data.
You can use a SparkPost stored template by setting a message's
:attr:`~anymail.message.AnymailMessage.template_id` to the
template's unique id. (When using a stored template, SparkPost prohibits
setting the EmailMessage's subject, text body, or html body.)
Alternatively, you can refer to merge fields directly in an EmailMessage's
subject, body, and other fields---the message itself is used as an
on-the-fly template.
In either case, supply the merge data values with Anymail's
normalized :attr:`~anymail.message.AnymailMessage.merge_data`
and :attr:`~anymail.message.AnymailMessage.merge_global_data`
message attributes.
.. code-block:: python
message = EmailMessage(
...
to=["alice@example.com", "Bob <bob@example.com>"]
)
message.template_id = "11806290401558530" # SparkPost 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",
# Can use SparkPost's special "dynamic" keys for nested substitutions (see notes):
'dynamic_html': {
'status_html': "<a href='https://example.com/order/{{order_no}}'>Status</a>",
},
'dynamic_plain': {
'status_plain': "Status: https://example.com/order/{{order_no}}",
},
}
See `SparkPost's substitutions reference`_ for more information on templates and
batch send with SparkPost. If you need the special `"dynamic" keys for nested substitutions`_,
provide them in Anymail's :attr:`~anymail.message.AnymailMessage.merge_global_data`
as shown in the example above. And if you want `use_draft_template` behavior, specify that
in :ref:`esp_extra <sparkpost-esp-extra>`.
.. _SparkPost's substitutions reference:
https://developers.sparkpost.com/api/substitutions-reference
.. _"dynamic" keys for nested substitutions:
https://developers.sparkpost.com/api/substitutions-reference#header-links-and-substitution-expressions-within-substitution-values
.. _sparkpost-webhooks:
Status tracking webhooks
------------------------
If you are using Anymail's normalized :ref:`status tracking <event-tracking>`, set up the
webhook in your `SparkPost account settings under "Webhooks"`_:
* Target URL: :samp:`https://{yoursite.example.com}/anymail/sparkpost/tracking/`
* Authentication: choose "Basic Auth." For username and password enter the two halves of the
*random:random* shared secret you created for your :setting:`ANYMAIL_WEBHOOK_SECRET`
Django setting. (Anymail doesn't support OAuth webhook auth.)
* Events: click "Select" and then *clear* the checkbox for "Relay Events" category (which is for
inbound email). You can leave all the other categories of events checked, or disable
any you aren't interested in tracking.
SparkPost will report these Anymail :attr:`~anymail.signals.AnymailTrackingEvent.event_type`\s:
queued, rejected, bounced, deferred, delivered, opened, clicked, complained, unsubscribed,
subscribed.
The event's :attr:`~anymail.signals.AnymailTrackingEvent.esp_event` field will be
a single, raw `SparkPost event`_. (Although SparkPost calls webhooks with batches of events,
Anymail will invoke your signal receiver separately for each event in the batch.)
The esp_event is the raw, `wrapped json event structure`_ as provided by SparkPost:
`{'msys': {'<event_category>': {...<actual event data>...}}}`.
.. _SparkPost account settings under "Webhooks":
https://app.sparkpost.com/account/webhooks
.. _SparkPost event:
https://support.sparkpost.com/customer/portal/articles/1976204-webhook-event-reference
.. _wrapped json event structure:
https://support.sparkpost.com/customer/en/portal/articles/2311698-comparing-webhook-and-message-event-data
.. _sparkpost-inbound:
Inbound webhook
---------------
If you want to receive email from SparkPost through Anymail's normalized :ref:`inbound <inbound>`
handling, follow SparkPost's `Enabling Inbound Email Relaying`_ guide to set up
Anymail's inbound webhook.
The target parameter for the Relay Webhook will be:
:samp:`https://{random}:{random}@{yoursite.example.com}/anymail/sparkpost/inbound/`
* *random:random* is an :setting:`ANYMAIL_WEBHOOK_SECRET` shared secret
* *yoursite.example.com* is your Django site
.. _Enabling Inbound Email Relaying:
https://www.sparkpost.com/docs/tech-resources/inbound-email-relay-webhook/