Pigeon Atlas
← Integrations

Send email from Django

A custom email backend of thirty lines sends every send_mail and EmailMessage through Pigeon Atlas over HTTP. Views, forms and the admin's password reset need no changes.

Updated September 15, 2026

Django routes all mail — send_mail, EmailMessage, the admin's password reset, mail_admins — through the class named in EMAIL_BACKEND. The default is SMTP. A backend that posts to Pigeon Atlas instead is one class with one method, and nothing that calls send_mail has to know.

The backend

# yourapp/mail/pigeonatlas.py
import json
import urllib.request

from django.conf import settings
from django.core.mail.backends.base import BaseEmailBackend

ENDPOINT = "https://pigeonatlas.com/v1/emails"


class PigeonAtlasBackend(BaseEmailBackend):
    def send_messages(self, email_messages):
        sent = 0
        for message in email_messages:
            html = next((body for body, kind in getattr(message, "alternatives", []) if kind == "text/html"), None)
            payload = {
                "from": message.from_email,
                "to": message.to,
                "subject": message.subject,
                "text": message.body,
                "reply_to": message.reply_to[0] if message.reply_to else None,
            }
            if html:
                payload["html"] = html
            payload = {k: v for k, v in payload.items() if v}

            request = urllib.request.Request(
                ENDPOINT,
                data=json.dumps(payload).encode(),
                headers={
                    "Authorization": f"Bearer {settings.PIGEONATLAS_API_KEY}",
                    "Content-Type": "application/json",
                },
                method="POST",
            )
            try:
                with urllib.request.urlopen(request, timeout=15):
                    sent += 1
            except Exception:
                if not self.fail_silently:
                    raise
        return sent

Point Django at it:

# settings.py
EMAIL_BACKEND = "yourapp.mail.pigeonatlas.PigeonAtlasBackend"
PIGEONATLAS_API_KEY = os.environ["PIGEONATLAS_API_KEY"]
DEFAULT_FROM_EMAIL = "Your App <hello@mail.yourapp.com>"

send_messages returning the count is the contract send_mail expects, and fail_silently is honoured the way the SMTP backend honours it. In tests Django swaps in the locmem backend on its own, so mail.outbox keeps working and nothing leaves a test run.

Several recipients

The API sends one message per recipient, and the loop above posts one request per EmailMessage, to its first recipient. If you send to lists, loop over message.to inside and post once each — the API treats every recipient as a separate message with its own delivery record, which is what you want for a receipt and not what you want for a newsletter (use the broadcast feature for those).

Retries

If you send from Celery tasks that retry, add an Idempotency-Key header built from the thing you are sending about — the order id, the user id and purpose — so a retried task returns the original message rather than sending twice. urllib is enough; there is no SDK to install, and requests works the same way if you already have it.

Before it works

DEFAULT_FROM_EMAIL must be on a domain you added and verified in Pigeon Atlas; otherwise every send is refused with 403 validation_error naming the domain. Publish the DKIM, SPF and MX records the domain page shows and wait for the ticks. Use a subdomain, keep the key in the environment, and read the API reference for attachments and the delivery webhook.

A thousand a month free, no card

Enough to verify a domain and run a small app from it.

Start sending

Other platforms