Framework guides
Send email from Django
Send transactional email from Django with the requests library — as a service function and as a Celery task.
This is the same POST /api/v1/emails endpoint documented in
Send a transactional email, wired into
a Django project as a plain service function and, for anything triggered
by a request, a Celery task.
Service function#
# core/bytloop_mail.py
import requests
from django.conf import settings
class BytloopMailError(Exception):
pass
def send_email(*, from_addr, to, subject, html, idempotency_key, tags=None):
response = requests.post(
"https://mail.bytloop.com/api/v1/emails",
headers={
"Authorization": f"Bearer {settings.BYTLOOP_API_KEY}",
"Content-Type": "application/json",
"Idempotency-Key": idempotency_key,
},
json={
"from": from_addr,
"to": to if isinstance(to, list) else [to],
"subject": subject,
"html": html,
"tags": tags or {},
},
timeout=15,
)
if not response.ok:
raise BytloopMailError(
f"Bytloop Mail send failed ({response.status_code}): {response.text}"
)
return response.json()# settings.py
import os
BYTLOOP_API_KEY = os.environ["BYTLOOP_API_KEY"]Call it directly for a one-off send, e.g. from a view after an order is placed:
from core.bytloop_mail import send_email
send_email(
from_addr="Support <support@send.acme.com>",
to=order.customer_email,
subject=f"Your receipt for order #{order.id}",
html=render_to_string("receipts/email.html", {"order": order}),
idempotency_key=f"receipt-{order.id}",
tags={"receipt_for_order": str(order.id)},
)Celery task (recommended)#
Calling requests.post synchronously inside a view blocks the response
on the API round-trip. If the project already runs Celery (common for
anything with background work), wrap the send in a task so the view
returns immediately and a transient failure retries automatically:
# core/tasks.py
from celery import shared_task
from django.template.loader import render_to_string
from core.bytloop_mail import BytloopMailError, send_email
@shared_task(
bind=True,
max_retries=5,
default_retry_delay=30,
)
def send_receipt_email(self, order_id):
from orders.models import Order # local import avoids a circular import at module load
order = Order.objects.get(pk=order_id)
try:
send_email(
from_addr="Support <support@send.acme.com>",
to=order.customer_email,
subject=f"Your receipt for order #{order.id}",
html=render_to_string("receipts/email.html", {"order": order}),
idempotency_key=f"receipt-{order.id}",
tags={"receipt_for_order": str(order.id)},
)
except BytloopMailError as exc:
raise self.retry(exc=exc)# wherever the order is confirmed
from core.tasks import send_receipt_email
send_receipt_email.delay(order.id)Common pitfalls#
- Calling
send_emailsynchronously from a view. Blocks the request on the API round-trip for no reason the user can see. Use the Celery task above for anything triggered by a request. - No
max_retries/ noself.retry. Without it, a transient network error or a5xxduring a deploy silently drops the email instead of trying again a few seconds later. - Reading
BYTLOOP_API_KEYwithos.environinside the task instead ofsettings. Keeps the key resolution in one place (settings.py), so a missing env var fails fast at startup instead of inside a background worker where it's easy to miss.
Next#
- Register a webhook endpoint (a Django view) to hear about delivery, bounces, and opens.
- Read the rate limits page so a bulk send (e.g. a
management command iterating
Order.objects.filter(...)) doesn't get throttled mid-run.