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 AnymailUnsupportedFeature error for likely mismatches. (This is a best guess using Python’s mimetypes package. 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 tags and metadata features 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_clicks or track_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_metadata may 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 ANYMAIL settings as RESEND_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 ANYMAIL settings as RESEND_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.