How to send Django password reset emails
Short answer: Django's password reset view sends through whatever EMAIL_BACKEND you set. Add a small SendFox email backend, point EMAIL_BACKEND at it, set DEFAULT_FROM_EMAIL to an address on your verified domain, and include django.contrib.auth.urls. Free for 3,000 emails a month.
This works on Django 4.2 and later and needs the requests package (pip install requests).
Step 1: Get an API key
Sign up free and open Transactional. Your key is created on the first visit. Store it in the SENDFOX_API_KEY environment variable.
Step 2: Add the email backend
One API call per message. It sends the plain-text body and the HTML alternative when there is one, and raises with the API's explanation when something's wrong.
myproject/sendfox.py
import requests
from django.conf import settings
from django.core.mail.backends.base import BaseEmailBackend
API_URL = "https://api.sendfox.com/transactional/emails"
class SendFoxEmailBackend(BaseEmailBackend):
def send_messages(self, email_messages):
sent = 0
for message in email_messages:
if message.attachments:
raise ValueError("SendFox does not support attachments yet.")
payload = {
"from": message.from_email,
"to": message.to,
"cc": message.cc,
"bcc": message.bcc,
"reply_to": message.reply_to[0] if message.reply_to else None,
"subject": message.subject,
"text": message.body,
}
for content, mimetype in getattr(message, "alternatives", []):
if mimetype == "text/html":
payload["html"] = content
response = requests.post(
API_URL,
json={key: value for key, value in payload.items() if value},
headers={"Authorization": f"Bearer {settings.SENDFOX_API_KEY}"},
timeout=10,
)
if response.ok:
sent += 1
elif not self.fail_silently:
# Every error has a stable name and a fix, e.g. "domain_not_verified".
error = response.json()
raise RuntimeError(f"{error['name']}: {error['fix']}")
return sent
Step 3: Point Django at it
settings.py
import os
EMAIL_BACKEND = "myproject.sendfox.SendFoxEmailBackend"
SENDFOX_API_KEY = os.environ["SENDFOX_API_KEY"]
DEFAULT_FROM_EMAIL = "Acme <[email protected]>"
Step 4: Wire up password reset
Django's built-in views handle the whole flow: the form, the signed one-time link, and the "set a new password" page. Give the reset view an HTML template so the email isn't plain text only:
urls.py
from django.contrib.auth import views as auth_views
from django.urls import include, path
urlpatterns = [
path(
"accounts/password_reset/",
auth_views.PasswordResetView.as_view(
html_email_template_name="registration/password_reset_email.html",
),
name="password_reset",
),
path("accounts/", include("django.contrib.auth.urls")),
]
Put the HTML version of the email in templates/registration/password_reset_email.html. Django passes it protocol, domain, uid and token to build the link, the same as the plain-text template.
Step 5: Test, then verify your domain
python manage.py shell -c "from django.core.mail import send_mail; send_mail('Test', 'It works.', None, ['[email protected]'])"
Send to [email protected] first. Test addresses never reach a mailbox and never count against your quota, and [email protected] and [email protected] fake those outcomes so you can see your error handling and webhook fire. Until your domain is verified, send from [email protected], which delivers to the test addresses and your own account email.
Then add your domain under Account > Domains, paste the DNS records into your DNS provider and click verify. After that, send from any address on it.
Things to know
- Plain addresses in
to. Pass[email protected], notAda <[email protected]>. The display name goes onfrom. - Attachments aren't supported yet, so the backend raises rather than sending without them.
- Send in the background (Celery, RQ or Django's task framework) if you don't want a request to wait on the API.
- Reset links are safe from scanners. Click tracking stays off unless you turn it on for a tag, and it can never be turned on for tags like
password_reset.
Full reference: Transactional API docs, or the same docs as Markdown to hand to your coding agent.
FAQ
Why aren't my Django password reset emails sending?
In development Django often uses the console backend, which prints mail instead of sending it. In production you need a real EMAIL_BACKEND and a from address on a domain you have verified. With the backend on this page, a failed send raises an error that names the problem and the fix.
Does this work for send_mail() and EmailMessage too?
Yes. Every email Django sends goes through EMAIL_BACKEND, so send_mail(), EmailMessage, EmailMultiAlternatives and the auth views all use it.
Can I use SMTP instead?
No. SendFox does not offer SMTP, so Django's SMTP backend cannot send through it. The backend on this page uses the HTTPS API.
How much does it cost?
Free for 3,000 emails a month. $15 a month for 50,000.
Send your first email in two minutes.
Free for 3,000 emails a month. $15 a month for 50,000. Already on Resend? Change two environment variables.
Get your free API key