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 webhook posts. Recommended if you are using activity tracking, otherwise not necessary. (This is separate from Anymail’s WEBHOOK_SECRET setting.)

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_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.

Restricted characters in ``from_email`` display names

Resend’s API does not accept many email address display names (a.k.a. “friendly names” or “real names”) formatted according to the relevant standard (RFC 5322). Anymail implements a workaround for the to, cc, bcc and reply_to fields, but Resend rejects attempts to use this workaround for from_email display names.

These characters will cause problems in a From address display name:

  • Double quotes (") and some other punctuation characters can cause a “Resend API response 422” error complaining of an “Invalid `from` field”, or can result in a garbled From name (missing segments, additional punctuation inserted) in the resulting message.

  • A question mark immediately followed by any alphabetic character (e.g., ?u) will cause a “Resend API response 451” security error complaining that “The email payload contain invalid characters”. (This behavior prevents use of standard RFC 2047 encoded words in From display names—which is the workaround Anymail implements for other address fields.)

There may be other character combinations that also cause problems. If you need to include punctuation in a From display name, be sure to verify the results. (The issues were reported to Resend in October, 2023.)

Attachment filename determines content type

Resend determines the content type of an attachment from its filename extension.

If you try to send an attachment without a filename, Anymail will substitute “attachment.ext” using an appropriate .ext for the content type.

If you try to send an attachment whose content type doesn’t match its filename extension, Resend will change the content type to match the extension. (E.g., the filename “data.txt” will always be sent as “text/plain”, even if you specified a “text/csv” content type.)

No inline images

Resend’s API does not provide a mechanism to send inline content or to specify Content-ID for an attachment.

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 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.

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 does not currently support inbound email.

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.