How to Send Transactional Email from Django with ZidiMail
A complete guide to sending transactional email from a Django application using the ZidiMail REST API. Covers direct API calls, Django signals, Celery async tasks, and settings configuration.
Django ships with an email backend that points to SMTP by default. For transactional email at scale — welcome emails, password resets, order confirmations — you want an HTTP API, not raw SMTP. This guide shows you how to call the ZidiMail REST API from Django, wire it into signals, and offload sends to Celery for async processing.
Prerequisites
- A ZidiMail account (free, no credit card)
- A verified sending domain
- An API key from the ZidiMail dashboard
- Django 4.2 or later
- requests installed: pip install requests
1. Add credentials to settings
# settings.py
ZIDIMAIL_API_KEY = os.environ.get('ZIDIMAIL_API_KEY', '')
ZIDIMAIL_FROM = os.environ.get('ZIDIMAIL_FROM', 'hello@mail.yourdomain.com')
ZIDIMAIL_REPLY_TO = os.environ.get('ZIDIMAIL_REPLY_TO', 'support@yourdomain.com')# .env (use python-dotenv or your deployment env vars)
ZIDIMAIL_API_KEY=zm_live_your_full_key
ZIDIMAIL_FROM=hello@mail.yourdomain.com
ZIDIMAIL_REPLY_TO=support@yourdomain.com2. Create a reusable email utility
# yourapp/email_utils.py
import logging
import requests
from django.conf import settings
logger = logging.getLogger(__name__)
ZIDIMAIL_API = 'https://api.zidimails.com/v1/emails'
def send_email(to, subject, html, text='', reply_to=None, idempotency_key=None):
"""
Send a transactional email via ZidiMail.
Args:
to: str or list of str — recipient address(es)
subject: str
html: str — HTML body
text: str — plain-text fallback (recommended)
reply_to: str — optional reply-to address
idempotency_key: str — optional key to prevent duplicate sends on retry
Returns:
dict with 'id' key on success.
Raises:
requests.HTTPError on API error.
"""
if isinstance(to, str):
to = [to]
payload = {
'from': settings.ZIDIMAIL_FROM,
'to': to,
'subject': subject,
'html': html,
}
if text: payload['text'] = text
if reply_to: payload['reply_to'] = reply_to
elif hasattr(settings, 'ZIDIMAIL_REPLY_TO'):
payload['reply_to'] = settings.ZIDIMAIL_REPLY_TO
headers = {'Authorization': f'Bearer {settings.ZIDIMAIL_API_KEY}'}
if idempotency_key:
headers['X-Idempotency-Key'] = idempotency_key
try:
response = requests.post(
ZIDIMAIL_API,
json=payload,
headers=headers,
timeout=15,
)
response.raise_for_status()
return response.json()
except requests.HTTPError as exc:
logger.error('ZidiMail send failed: %s — %s', exc.response.status_code, exc.response.text)
raise3. Send a welcome email via a Django signal
Signals let you react to model events without coupling email logic into your views.
# yourapp/signals.py
from django.db.models.signals import post_save
from django.dispatch import receiver
from django.template.loader import render_to_string
from .models import UserProfile
from .email_utils import send_email
@receiver(post_save, sender=UserProfile)
def send_welcome_email(sender, instance, created, **kwargs):
if not created:
return
html = render_to_string('emails/welcome.html', {'user': instance.user})
text = f"Welcome {instance.user.first_name}! Visit https://app.yourdomain.com to get started."
send_email(
to=instance.user.email,
subject=f"Welcome to Acme, {instance.user.first_name}!",
html=html,
text=text,
idempotency_key=f"welcome-{instance.user.id}",
){# templates/emails/welcome.html #}
<!DOCTYPE html>
<html lang="en">
<body style="font-family:sans-serif;max-width:560px;margin:0 auto;padding:32px 16px;color:#1a1a1a;">
<h1 style="font-size:24px;">Welcome, {{ user.first_name }}!</h1>
<p>Your account is ready. Sign in to get started.</p>
<a href="https://app.yourdomain.com"
style="display:inline-block;background:#2563eb;color:#fff;padding:12px 24px;border-radius:8px;text-decoration:none;font-weight:600;">
Open your dashboard
</a>
</body>
</html>4. Offload to Celery for async sending
Email sends block the request thread. Use Celery to queue them in the background.
# Install: pip install celery redis
# yourapp/tasks.py
from celery import shared_task
from .email_utils import send_email
import logging
logger = logging.getLogger(__name__)
@shared_task(bind=True, max_retries=3, default_retry_delay=30)
def send_email_task(self, to, subject, html, text='', idempotency_key=None):
try:
return send_email(to, subject, html, text, idempotency_key=idempotency_key)
except Exception as exc:
logger.warning('Email send failed, retrying: %s', exc)
raise self.retry(exc=exc)
# Call from anywhere — does not block the web request:
# send_email_task.delay(
# to='user@example.com',
# subject='Order confirmed',
# html='<p>Your order #1234 is confirmed.</p>',
# idempotency_key='order-1234-confirmed',
# )5. Password reset example
# yourapp/views.py
from django.views.decorators.http import require_POST
from django.http import JsonResponse
from .tasks import send_email_task
from .models import PasswordResetToken
@require_POST
def request_password_reset(request):
email = request.POST.get('email', '').strip()
if not email:
return JsonResponse({'error': 'Email required'}, status=400)
token = PasswordResetToken.objects.create_for_email(email)
if token is None:
# Always return ok — do not reveal whether the email exists
return JsonResponse({'ok': True})
reset_url = f"https://app.yourdomain.com/reset-password?token={token.key}"
html = f"""
<p>Click below to reset your password. The link expires in 1 hour.</p>
<p><a href="{reset_url}" style="color:#2563eb;">Reset your password</a></p>
"""
send_email_task.delay(
to=email,
subject='Reset your password',
html=html,
text=f'Reset your password: {reset_url}',
idempotency_key=f'pwreset-{token.key}',
)
return JsonResponse({'ok': True})Common issues
- "422" with "domain not verified" — the from address domain must be added and verified in ZidiMail under Domains.
- requests.exceptions.Timeout — always set timeout=15 (seconds). Without it, a slow API response hangs your worker indefinitely.
- Signal fires but email not sent — confirm signals.py is imported in your AppConfig.ready() method.
- Celery task retried but email sent twice — pass idempotency_key built from a stable ID (user ID, order ID) to deduplicate.
Start sending from Django today
Free account, verified domain, and your first transactional email in under 30 minutes.
Start sending free