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.
pip install "django-anymail[postmark]"(or[sendgrid],[mailgun],[brevo],[amazon-ses]).- Add
anymailtoINSTALLED_APPSand setEMAIL_BACKEND = "anymail.backends.postmark.EmailBackend". - Put the API key in
ANYMAIL = {"POSTMARK_SERVER_TOKEN": env("POSTMARK_SERVER_TOKEN")}. - Verify your sending domain with the provider (SPF and DKIM records), and set
DEFAULT_FROM_EMAILon that domain. - Add Anymail's tracking webhook URL and handle
trackingsignals to suppress bounced addresses. - 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.
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.
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_idand 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; keepADMINSsmall.
Next steps
- Make sure messages arrive with the Django Email Deliverability Checklist.
- If nothing arrives, start with Fix Django Emails Not Sending in Production.
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.