Postmark
Anymail integrates with the ActiveCampaign Postmark transactional email service, using their HTTP email API.
Settings
EMAIL_BACKEND
To use Anymail’s Postmark backend, set:
EMAIL_BACKEND = "anymail.backends.postmark.EmailBackend"
in your settings.py.
POSTMARK_SERVER_TOKEN
Required. A Postmark server token.
ANYMAIL = { ... "POSTMARK_SERVER_TOKEN": "<your server token>", }
Anymail will also look for POSTMARK_SERVER_TOKEN at the
root of the settings file if neither ANYMAIL["POSTMARK_SERVER_TOKEN"]
nor ANYMAIL_POSTMARK_SERVER_TOKEN is set.
You can override the server token for an individual message in its esp_extra.
POSTMARK_USE_BULK_API
Added in version 15.2.
Set True to use Postmark’s bulk API for all sending. (This is not
recommended if you also send transactional emails; instead, enable the bulk API
only for specific sends.) See Using Postmark’s bulk API below.
POSTMARK_MESSAGE_STREAM
Added in version 15.2.
The Postmark message stream ID to use for sending. If not set, Postmark will use the default transactional message stream for most sends, or the default broadcast message stream for bulk API sends.
To override the message stream for particular sends, include
"message_stream" in the MAILERS options for a particular mailer
configuration (Django 6.1 or later) or pass message_stream="<id>" to
django.core.mail.get_connection(). You can also provide a
"MessageStream" (note the capitalization) in Anymail’s
esp_extra on particular message.
POSTMARK_API_URL
The base url for calling the Postmark API.
The default is POSTMARK_API_URL = "https://api.postmarkapp.com/"
(It’s unlikely you would need to change this.)
esp_extra support
To use Postmark 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 Postmark’s
email API.
Example:
message.esp_extra = { 'FuturePostmarkOption': 'value', 'server_token': '<API server token for just this message>', }
(You can also set "esp_extra" in Anymail’s
global send defaults to apply it to all
messages.)
Limitations and quirks
Postmark does not support a few tracking and reporting additions offered by other ESPs.
Anymail normally raises an AnymailUnsupportedFeature
error when you try to send a message using features that Postmark doesn’t support
You can tell Anymail to suppress these errors and send the messages anyway –
see Unsupported features.
- Single tag
Postmark allows a maximum of one tag per message. If your message has two or more
tags, you’ll get anAnymailUnsupportedFeatureerror—or if you’ve enabledANYMAIL_IGNORE_UNSUPPORTED_FEATURES, Anymail will use only the first tag.- No delayed sending
Postmark does not support
send_at.- Click-tracking
Postmark supports several link-tracking options. Anymail treats
track_clicksas Postmark’s “HtmlAndText” option when True.If you would prefer Postmark’s “HtmlOnly” or “TextOnly” link-tracking, you could either set that as a Postmark server-level default (and use
message.track_clicks = Falseto disable tracking for specific messages), or use something likemessage.esp_extra = {'TrackLinks': "HtmlOnly"}to specify a particular option.
- Open-tracking
To control
track_openson individual messages, you must disable Postmark’s server-level default and then settrack_opens = Trueon all messages that should have open tracking. (A message-leveltrack_opens = Falsecannot override open tracking if enabled in Postmark’s server defaults.)If most of your messages should be sent with open tracking, you can use Anymail’s global send defaults (rather than Postmark’s server-level setting):
# settings.py ANYMAIL = { # ... "SEND_DEFAULTS": { "track_opens": True }, }
Individual messages can then use
track_opens = Falseto override Anymail’s default.
- No envelope sender overrides
Postmark does not support overriding
envelope_senderon individual messages. (You can configure custom return paths for each sending domain in the Postmark control panel.)- No non-ASCII mailboxes (EAI)
Postmark does not support sending from or to Unicode mailboxes (the user part of user@domain—see EAI). Trying to use one will cause an API error.
Batch sending/merge and ESP templates
Postmark offers both ESP stored templates and batch sending with per-recipient merge data.
Changed in version 4.2: Added Postmark merge_data and batch sending
support. (Earlier Anymail releases only supported
merge_global_data with Postmark.)
To use a Postmark template, set the message’s
template_id to either the numeric Postmark
“TemplateID” or its string “TemplateAlias” (which is not the template’s name).
You can find a template’s numeric id near the top right in Postmark’s template editor,
and set the alias near the top right above the name.
Changed in version 5.0: Earlier Anymail releases only allowed numeric template IDs.
Supply the Postmark “TemplateModel” variables 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 = 80801 # Postmark template id or alias 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", }
Postmark does not allow overriding the message’s subject or body with a template. (You can customize the subject by including variables in the template’s subject.)
When you supply per-recipient merge_data,
Anymail automatically switches to Postmark’s batch send API, so that
each “to” recipient sees only their own email address. (Any cc’s or bcc’s will be
duplicated for every to-recipient.)
If you want to use batch sending with a regular message (without a template), set
merge data to an empty dict: message.merge_data = {}.
Postmark only applies merge data through its template APIs or with the bulk API,
so non-empty merge_data or
merge_global_data requires a
template_id (or enabling the bulk API).
Anymail raises an AnymailUnsupportedFeature error otherwise,
rather than sending a message where the merge data would be silently ignored.
See this Postmark blog post on templates for more information.
Using Postmark’s bulk API
Added in version 15.2.
Postmark supports a separate bulk API which is optimized for large-volume
broadcast sending. By default, Anymail uses one of Postmark’s transactional
sending APIs. You can enable bulk sending for all sending with the
ANYMAIL configuration option POSTMARK_USE_BULK_API, or use one of the approaches described below
to use the bulk API only for specific sends.
Postmark currently requires customers to request approval for the bulk API. Trying to send without that approval will result in a 422 error.
When the bulk API is used, Anymail treats all sends as batch sending (whether or not merge_data or some other batch attribute
is used). That is, each to address results in a separate message, and
multiple to recipients do not see the other to addresses. (cc and
bcc are duplicated to each to recipient.)
Postmark’s bulk API allows customizing the message for each recipient, either
with a template_id or using inline {{ variable }} substitutions
in the body and subject. In either case, use Anymail’s merge_data and
merge_global_data to supply the substitutions.
The bulk API may delay sending, so is not suitable for high-priority transactional emails like password resets or order confirmations. It’s best to direct different types of email to the appropriate API. Using multiple mailer configurations (available starting in Django 6.1) is recommended for this. For example:
MAILERS = {
# Configure the default mailer for transactional sending:
"default": {
"BACKEND": "anymail.backends.postmark.EmailBackend",
"OPTIONS": {
"server_token": ...,
},
},
# Create a second mailer for broadcast sending with the bulk API:
"announcements": {
"BACKEND": "anymail.backends.postmark.EmailBackend",
"OPTIONS": {
"server_token": ...,
"use_bulk_api": True,
# Set the Postmark message stream ID if needed. If not set,
# Postmark's bulk API will use your default broadcast stream.
"message_stream": "broadcast-announcements",
},
},
}
With that configuration, you can direct particular emails to the bulk API with
using="announcements" in the send call.
Prior to Django 6.1, to use the bulk API only for specific messages, don’t set
POSTMARK_USE_BULK_API in your
Anymail settings. Instead, call django.core.mail.get_connection() with
use_bulk_api=True and pass the resulting connection to Django’s sending
functions or EmailMessage constructor .
Because the bulk API may defer sending, Postmark’s Message-ID is not
available at send time. To facilitate status tracking, Anymail will generate a
unique ID for each to recipient in a bulk send and include it as
per-recipient metadata (the "anymail_id" metadata
field is reserved for this purpose). The generated IDs are available in the
message.anymail_status.recipients dict
after sending and in tracking webhooks as event.message_id.
Status tracking webhooks
If you are using Anymail’s normalized status tracking, set up a webhook in your Postmark account settings, under Servers > your server name > Settings > Webhooks. The webhook URL is:
https://random:random@yoursite.example.com/anymail/postmark/tracking/
random:random is an
ANYMAIL_WEBHOOK_SECRETshared secretyoursite.example.com is your Django site
Choose all the event types you want to receive. Anymail doesn’t care about the “include messsage content” and “post only on first open” options; whether to use them is your choice.
If you use multiple Postmark servers, you’ll need to repeat entering the webhook settings for each of them.
Postmark will report these Anymail event_types:
rejected, failed, bounced, deferred, delivered, autoresponded, opened, clicked, complained,
unsubscribed, subscribed. (Postmark does not support sent–what it calls “processed”–events
through webhooks.)
The event’s esp_event field will be
a dict of Postmark delivery,
bounce,
spam-complaint,
open-tracking, or
click data.
Inbound webhook
To receive email from Postmark through Anymail’s normalized inbound handling, follow Postmark’s guide to Configure an inbound server that posts to Anymail’s inbound webhook.
In their step 4, set the inbound webhook URL to:
https://random:random@yoursite.example.com/anymail/postmark/inbound/
random:random is an
ANYMAIL_WEBHOOK_SECRETshared secretyoursite.example.com is your Django site
We recommend enabling the “Include raw email content in JSON payload” checkbox.
Anymail’s inbound handling supports either choice, but raw email is preferred
to get the most accurate representation of any received message. (If you are using
Postmark’s server API, this is the RawEmailEnabled option.)
Changed in version 10.0: Added handling for Postmark’s “include raw email content”.
You may also want to read through the “Inbound domain forwarding” and “Configure inbound blocking” sections of Postmark’s Inbound Processing guide.