Resend
Anymail integrates Django with the Resend transactional email service, using their send-email API endpoint.
Added in version 10.2.
Installation
Anymail uses the svix package to validate Resend webhook signatures.
If you will use Anymail’s status tracking webhook
with Resend, and you want to use webhook signature validation, be sure
to include the [resend] option when you install Anymail:
$ python -m pip install 'django-anymail[resend]'
(Or separately run python -m pip install svix.)
The svix package pulls in several other dependencies, so its use
is optional in Anymail. See Status tracking webhooks below for details.
To avoid installing svix with Anymail, just omit the [resend] option.
Settings
EMAIL_BACKEND
To use Anymail’s Resend backend, set:
EMAIL_BACKEND = "anymail.backends.resend.EmailBackend"
in your settings.py.
RESEND_API_KEY
Required for sending. An API key from your Resend API Keys. Anymail needs only “sending access” permission; “full access” is not recommended.
ANYMAIL = { ... "RESEND_API_KEY": "re_...", }
Anymail will also look for RESEND_API_KEY at the
root of the settings file if neither ANYMAIL["RESEND_API_KEY"]
nor ANYMAIL_RESEND_API_KEY is set.
RESEND_SIGNING_SECRET
The Resend webhook signing secret used to verify tracking webhook posts.
Recommended if you are using activity tracking, otherwise not necessary.
(This is separate from Anymail’s
WEBHOOK_SECRET setting and the
RESEND_INBOUND_SECRET used for
securing inbound email.received webhook posts.)
Find this in your Resend Webhooks settings: after adding a webhook, click into its management page and look for “signing secret” near the top.
ANYMAIL = { ... "RESEND_SIGNING_SECRET": "whsec_...", }
If you provide this setting, the svix package is required. See Installation above.
RESEND_INBOUND_SECRET
The Resend webhook signing secret used to verify inbound email.received
webhook posts. Recommended if you are using inbound email, otherwise not
necessary. (This is separate from Anymail’s WEBHOOK_SECRET setting.)
Find this in your Resend Webhooks settings: after adding an inbound webhook, click into its management page and look for “signing secret” near the top.
ANYMAIL = { ... "RESEND_INBOUND_SECRET": "whsec_...", }
If you provide this setting, the svix package is required. See Installation above.
RESEND_API_URL
The base url for calling the Resend API.
The default is RESEND_API_URL = "https://api.resend.com/".
(It’s unlikely you would need to change this.)
Limitations and quirks
Resend does not support a few features offered by some other ESPs, and can have unexpected behavior for some common use cases.
Anymail normally raises an AnymailUnsupportedFeature
error when you try to send a message using features that Resend doesn’t support.
You can tell Anymail to suppress these errors and send the messages
anyway—see Unsupported features.
- Attachment filename extension must match content type
Resend silently drops messages with attachments whose filename extensions are inconsistent with their content types (mimetype). E.g., sending a text/csv attachment with the filename “data.txt” rather than “data.csv” will not generate an API error or bounce, but the message will never be delivered.
To avoid this, Anymail attempts to verify attachment filenames before sending, and raises an
AnymailUnsupportedFeatureerror for likely mismatches. (This is a best guess using Python’smimetypespackage. There’s no way for Anymail to know exactly which extensions and content types will cause Resend to drop a message.)If you try to send an attachment without a filename, Anymail will generate a filename for you using “attachment.ext” with an appropriate extension for the content type.
Changed in version 14.0: Resend’s API did not previously support specifying the content type, and instead based attachment content type on the filename.
- Anymail tags and metadata are exposed to recipient
Anymail implements its normalized
tagsandmetadatafeatures for Resend using custom email headers. That means they can be visible to recipients via their email app’s “show original message” (or similar) command. Do not include sensitive data in tags or metadata.Resend also offers a feature it calls “tags”, which allows arbitrary key-value data to be tracked with a sent message (similar Anymail’s
metadata). Resend’s native tags are not exposed to recipients, but they have significant restrictions on character set and length (for both keys and values).If you want to use Resend’s native tags with Anymail, you can send them using esp_extra, and retrieve them in a status tracking webhook using esp_event. (The linked sections below include examples.)
- No click/open tracking overrides
Resend does not support
track_clicksortrack_opens. Its tracking features can only be configured at the domain level in Resend’s control panel.- No attachments with delayed sending
Resend does not support attachments or batch sending features when using
send_at.Changed in version 12.0: Resend now supports
send_at.- No attachments with batch sending
Resend does not currently support attachments when using batch sending. Trying to send an attachment while using
merge_metadatamay result in a Resend API error.- No envelope sender
Resend does not support specifying the
envelope_sender.- Status tracking does not identify recipient
If you send a message with multiple recipients (to, cc, and/or bcc), Resend’s status webhooks do not identify which recipient applies for an event. See the note below.
- No non-ASCII mailboxes (EAI)
Resend 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.
Earlier limitations
Changed in version 14.0.
Resend’s API did not previously support inline images. Earlier Anymail releases raised an error on attempts to send them through Resend.
API rate limits
Resend provides rate limit headers with each API call response.
To access them after a successful send, use (e.g.,)
message.anymail_status.esp_response.headers["ratelimit-remaining"].
If you exceed a rate limit, you’ll get an AnymailAPIError
with error.status_code == 429, and can determine how many seconds to wait
from error.response.headers["retry-after"].
exp_extra support
Anymail’s Resend backend will pass esp_extra
values directly to Resend’s send-email API. Example:
message = AnymailMessage(...) message.esp_extra = { # Use Resend's native "tags" feature # (be careful about character set restrictions): "tags": [ {"name": "Co_Brand", "value": "Acme_Inc"}, {"name": "Feature_Flag_1", "value": "test_22_a"}, ], }
Batch sending/merge and ESP templates
Added in version 10.3: Support for batch sending with
merge_metadata.
Resend supports batch sending (where each To recipient sees only their own email address). It also supports per-recipient metadata with batch sending.
Set Anymail’s normalized merge_metadata
attribute to use Resend’s batch-send API:
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"}, }
Resend does not currently offer ESP stored templates
or merge capabilities, so does not support Anymail’s
merge_data,
merge_global_data, or
template_id message attributes.
(Resend’s current template feature is only supported in node.js,
using templates that are rendered in their API client.)
(Setting merge_data to an empty
dict will also invoke batch send, but trying to supply merge data for
any recipient will raise an AnymailUnsupportedFeature error.)
Status tracking webhooks
Anymail’s normalized status tracking works with Resend’s webhooks.
Resend implements webhook signing, using the svix package for signature validation (see Installation above). You have three options for securing the status tracking webhook:
Use Resend’s webhook signature validation, by setting
RESEND_SIGNING_SECRET(requires the svix package)Use Anymail’s shared secret validation, by setting
WEBHOOK_SECRET(does not require svix)Use both
Signature validation is recommended, unless you do not want to add svix to your dependencies.
To configure Anymail status tracking for Resend, add a new webhook endpoint to your Resend Webhooks settings:
For the “Endpoint URL”, enter one of these (where yoursite.example.com is your Django site).
If are not using Anymail’s shared webhook secret:
https://yoursite.example.com/anymail/resend/tracking/Or if you are using Anymail’s
WEBHOOK_SECRET, include the random:random shared secret in the URL:https://random:random@yoursite.example.com/resend/tracking/For “Events to listen”, select any or all events you want to track.
Click the “Add” button.
Then, if you are using Resend’s webhook signature validation (with svix), add the webhook signing secret to your Anymail settings:
Still on the Resend Webhooks settings page, click into the webhook endpoint URL you added above, and copy the “signing secret” listed near the top of the page.
Add that to your settings.py
ANYMAILsettings asRESEND_SIGNING_SECRET:ANYMAIL = { # ... "RESEND_SIGNING_SECRET": "whsec_..." }
Resend will report these Anymail
event_types:
sent, delivered, bounced, deferred, complained, opened, and clicked.
Note
Multiple recipients not recommended with tracking
If you send a message with multiple recipients (to, cc, and/or bcc), you will receive separate events (delivered, bounced, opened, etc.) for every recipient. But Resend does not identify which recipient applies for a particular event.
The event.recipient
will always be the first to email, but the event might actually have been
generated by some other recipient.
To avoid confusion, it’s best to send each message to exactly one to
address, and avoid using cc or bcc.
The status tracking event’s esp_event
field will be the parsed Resend webhook payload. For example, if you provided
Resend’s native “tags” via esp_extra when sending,
you can retrieve them in your tracking signal receiver like this:
@receiver(tracking)
def handle_tracking(sender, event, esp_name, **kwargs):
...
resend_tags = event.esp_event.get("tags", {})
# resend_tags will be a flattened dict (not
# the name/value list used when sending). E.g.:
# {"Co_Brand": "Acme_Inc", "Feature_Flag_1": "test_22_a"}
Inbound
Resend supports inbound email. See the document Resend Receiving Emails for more information. You can use a default or custom domain.
To configure Anymail inbound handling for Resend, add a new webhook endpoint to your Resend Webhooks settings:
For the “Endpoint URL”, enter one of these (where yoursite.example.com is your Django site).
If are not using Anymail’s shared webhook secret:
https://yoursite.example.com/anymail/resend/inbound/Or if you are using Anymail’s
WEBHOOK_SECRET, include the random:random shared secret in the URL:https://random:random@yoursite.example.com/resend/inbound/For “Events to listen”, select
email.received.Click the “Add” button.
Then, if you are using Resend’s webhook signature validation (with svix), add the inbound webhook signing secret to your Anymail settings:
Still on the Resend Webhooks settings page, click into the webhook endpoint URL you added above, and copy the “signing secret” listed near the top of the page.
Add that to your settings.py
ANYMAILsettings asRESEND_INBOUND_SECRET:ANYMAIL = { # ... "RESEND_INBOUND_SECRET": "whsec_..." }
If you are using both inbound email.received and analytics tracking
webhooks, note that they use different signing secrets, so have different
Anymail settings. Don’t mix up the two.
Troubleshooting
If Anymail’s Resend integration isn’t behaving like you expect, Resend’s dashboard includes diagnostic logs that can help isolate the problem:
Resend Logs page lists every call received by Resend’s API
Resend Emails page shows every event related to email sent through Resend
Resend Webhooks page shows every attempt by Resend to call your webhook (click into a webhook endpoint url to see the logs for that endpoint)
See Anymail’s Troubleshooting docs for additional suggestions.