Deployment
Email delivery

Send Django Email Through SendGrid, Postmark, or Mailgun With Anymail

Use django-anymail to send Django email through an email API such as SendGrid, Postmark, Mailgun, Brevo, or Amazon SES: configure the backend, verify the sending domain, handle tracking webhooks and bounces, and test without sending.

Problem statement

Sending transactional email from Django through raw SMTP works, but email providers' HTTP APIs are faster, avoid blocked SMTP ports on cloud hosts, and offer delivery events (bounces, complaints, opens). Each provider has its own SDK and payload format, though, and switching providers later means rewriting code. django-anymail gives Django a single email backend for many providers, keeping send_mail and EmailMessage unchanged, and normalises webhook events so bounce handling does not depend on the vendor.

Email via Anymailsend_mail()standard Django APIAnymail backendprovider adapterProvider APIHTTPS, not SMTPWebhooksbounces, complaintsDjango code stays the same; Anymail translates messages and delivery events per provider.
  1. pip install "django-anymail[postmark]" (or [sendgrid], [mailgun], [brevo], [amazon-ses]).
  2. Add anymail to INSTALLED_APPS and set EMAIL_BACKEND = "anymail.backends.postmark.EmailBackend".
  3. Put the API key in ANYMAIL = {"POSTMARK_SERVER_TOKEN": env("POSTMARK_SERVER_TOKEN")}.
  4. Verify your sending domain with the provider (SPF and DKIM records), and set DEFAULT_FROM_EMAIL on that domain.
  5. Add Anymail's tracking webhook URL and handle tracking signals to suppress bounced addresses.
  6. Send asynchronously from Celery for anything in a request path.

Step-by-step solution

1. Install and configure the backend

INSTALLED_APPS += ["anymail"]

EMAIL_BACKEND = "anymail.backends.postmark.EmailBackend"
ANYMAIL = {
    "POSTMARK_SERVER_TOKEN": env("POSTMARK_SERVER_TOKEN"),
    "WEBHOOK_SECRET": env("ANYMAIL_WEBHOOK_SECRET"),   # "user:password" for basic auth on webhooks
}
DEFAULT_FROM_EMAIL = "Example <no-reply@mail.example.com>"
SERVER_EMAIL = "errors@mail.example.com"

Changing providers later is mostly a matter of changing the backend path and settings keys:

EMAIL_BACKEND = "anymail.backends.sendgrid.EmailBackend"
ANYMAIL = {"SENDGRID_API_KEY": env("SENDGRID_API_KEY")}

2. Send email as usual

from django.core.mail import send_mail, EmailMultiAlternatives

send_mail("Your receipt", text_body, None, [user.email])

msg = EmailMultiAlternatives("Welcome", text_body, to=[user.email])
msg.attach_alternative(html_body, "text/html")
msg.tags = ["welcome"]                       # Anymail extension: provider tags
msg.metadata = {"user_id": str(user.pk)}     # Anymail extension: returned in webhooks
msg.send()

msg.anymail_status contains the provider's message ID and status after sending, useful for logging.

same Django code, different providersBackendSettings keyPostmarkanymail.backends.postmarkPOSTMARK_SERVER_TOKENSendGridanymail.backends.sendgridSENDGRID_API_KEYMailgunanymail.backends.mailgunMAILGUN_API_KEY + domainAmazon SESanymail.backends.amazon_sesAWS credentials / roleBrevoanymail.backends.brevoBREVO_API_KEYSwitching providers changes settings, not application codedjango-deployment.com
Each backend path ends in .EmailBackend in settings.

3. Authenticate your domain

Add the provider's SPF include and DKIM records to DNS for the sending domain (a subdomain such as mail.example.com keeps reputation separate from your main domain), and publish a DMARC policy. Until the domain is verified, providers either refuse to send or deliver to spam.

4. Handle bounces and complaints

# urls.py
path("anymail/", include("anymail.urls")),   # e.g. /anymail/postmark/tracking/

Configure that URL (with basic auth credentials from WEBHOOK_SECRET) in the provider's dashboard, then react to normalised events:

from anymail.signals import tracking
from django.dispatch import receiver

@receiver(tracking)
def handle_email_event(sender, event, esp_name, **kwargs):
    if event.event_type in ("bounced", "complained", "rejected"):
        Suppression.objects.update_or_create(email=event.recipient, defaults={"reason": event.event_type})

Check the suppression list before sending, so you stop mailing addresses that bounce, which protects your sender reputation.

5. Send asynchronously and test safely

Calls to the provider take tens to hundreds of milliseconds and can fail. Send from Celery tasks with retries (see Send Django Email in the Background). In tests and staging, use anymail.backends.test.EmailBackend or Django's locmem backend so nothing reaches real inboxes.

Anymail in productionBackend and API key from envno keys in settings filesSending domain verifiedSPF, DKIM, DMARCTracking webhook with basic authbounces, complaintsSuppression list checkedprotect reputationTest backend outside productionno real emails from stagingDelivery depends as much on DNS and bounce handling as on codedjango-deployment.com
Watch bounce and complaint rates in the provider dashboard weekly.

Verification check: python manage.py sendtestemail you@example.com delivers to your inbox with DKIM pass in the headers, the provider's activity log shows the message, and a test bounce (providers offer test addresses) creates a suppression record.

Explanation

Anymail implements Django's email backend interface and converts EmailMessage objects into each provider's API payload, including attachments, inline images, tags, metadata, and templates. Inbound tracking webhooks are parsed into a common AnymailTrackingEvent, so bounce handling code works across providers. Using HTTPS APIs avoids SMTP port restrictions common on cloud providers and returns message IDs immediately, which makes delivery issues traceable.

Edge cases / notes

  • Provider templates can be used with msg.template_id and merge data; keep fallback text bodies.
  • Rate limits vary by provider and plan; queue bulk sends and respect limits.
  • Transactional vs marketing. Use separate streams or subdomains so newsletters do not hurt receipts' deliverability.
  • Admin error emails (mail_admins) go through the same backend; keep ADMINS small.

Next steps

FAQ

What is django-anymail?

A Django email backend that sends through many email service providers' APIs with one interface.

Do I need to change send_mail calls to use Anymail?

No. Change EMAIL_BACKEND and settings; existing send_mail and EmailMessage code keeps working.

How do I handle bounces with Anymail?

Add the tracking webhook URL to your provider and handle Anymail's tracking signal.

Is an email API better than SMTP?

Usually: faster, not blocked by cloud SMTP restrictions, and with delivery events. SMTP still works if you prefer it.

Keep going