Django integration: send transactional email
Updated
There is no Inboxili Django-specific package. You can use the official Python package `inboxili` (see its page) or the plain HTTP code below.
Django's send_mail goes through EMAIL_BACKEND, which is SMTP by default. Inboxili is API-only, so you call it from a service function instead.
Settings
settings.py:
import os
INBOXILI_API_KEY = os.environ["INBOXILI_API_KEY"]
INBOXILI_BASE_URL = os.environ.get("INBOXILI_BASE_URL", "https://api.inboxili.com/api/v1")
INBOXILI_FROM_EMAIL = os.environ.get("INBOXILI_FROM_EMAIL", "hello@yourdomain.com")
INBOXILI_FROM_NAME = "Acme"
The service
mail/inboxili.py:
import requests
from django.conf import settings
class InboxiliError(Exception):
def __init__(self, status, code, message):
super().__init__(message)
self.status, self.code = status, code
def send(*, to, subject=None, html_body=None, text_body=None, template_id=None, template_data=None,
from_email=None, from_name=None):
payload = {
"to": to,
"from_email": from_email or settings.INBOXILI_FROM_EMAIL,
"from_name": from_name or settings.INBOXILI_FROM_NAME,
"subject": subject,
"html_body": html_body,
"text_body": text_body,
"template_id": template_id,
"template_data": template_data or {},
}
payload = {k: v for k, v in payload.items() if v is not None}
resp = requests.post(
f"{settings.INBOXILI_BASE_URL}/transactional/send",
json=payload,
headers={"Authorization": f"Bearer {settings.INBOXILI_API_KEY}"},
timeout=10,
)
if not resp.ok:
err = (resp.json() if resp.content else {}).get("error", {})
raise InboxiliError(resp.status_code, err.get("code", "http_error"), err.get("message", resp.reason))
return resp.json()
Send from a Celery task
from celery import shared_task
from django.contrib.auth import get_user_model
from mail import inboxili
@shared_task(bind=True, max_retries=4)
def send_welcome_email(self, user_id):
user = get_user_model().objects.get(pk=user_id)
try:
inboxili.send(
to=user.email,
subject="Welcome, {{first_name}}",
html_body="<p>Hi {{first_name}}, your account is ready.</p>",
template_data={"first_name": user.first_name or "there"},
)
except inboxili.InboxiliError as e:
if e.status == 429:
raise self.retry(exc=e, countdown=2 ** self.request.retries * 5)
raise
Trigger it after the transaction commits so the task never runs against a user row that was rolled back:
from django.db import transaction
transaction.on_commit(lambda: send_welcome_email.delay(user.pk))
Error handling
| status | code | Do |
|---|---|---|
| 401 | unauthorized | Check the key. |
| 403 | forbidden | Check scope and IP rules. |
| 422 | validation_error, sender_not_verified, send_failed | Fix the request. |
| 429 | rate_limited | Retry with backoff (done above). |
requests.Timeout means the outcome is unknown. With no idempotency key, retrying it can send a second email.
Testing
from unittest.mock import patch, MagicMock
from django.test import TestCase, override_settings
from mail import inboxili
@override_settings(INBOXILI_API_KEY="ik_live_test")
class InboxiliTests(TestCase):
@patch("mail.inboxili.requests.post")
def test_send(self, post):
post.return_value = MagicMock(ok=True, json=lambda: {"status": "sent", "message_id": "m1"})
out = inboxili.send(to="a@b.co", subject="s", html_body="<p>x</p>")
self.assertEqual(out["message_id"], "m1")
self.assertEqual(post.call_args.kwargs["json"]["to"], "a@b.co")
Production notes
- Keep the key in the environment, not in
settings.py. - Keep Django's
EMAIL_BACKENDfor admin error mails, if you use them, but be aware those do not go through Inboxili. - Webhooks: use a csrf-exempt view and compute the hex HMAC-SHA256 of
request.bodywith your webhook secret and compare it to theX-Inboxili-Signatureheader usinghmac.compare_digest.
Frequently asked questions
- Can I use Django's send_mail with Inboxili?
- Not directly. send_mail goes through an EMAIL_BACKEND, and Inboxili has no SMTP endpoint. You could write a custom backend that calls the API, but it could not carry attachments, CC or BCC because the API has no fields for them.
Build with Inboxili
Create a workspace, verify a domain, and make your first API call.