Send email from Django

Five steps: install requests, create a test key, send to the sandbox from a view, read the log, then verify a domain from a management command so you can send to anyone.

1. Install

No SDK needed. The API is plain JSON over HTTPS, so requests is all it takes. Install it in your project's environment.

shell
pip install requests

2. Create an API key

Sign in, open API keys in the dashboard and create a test key. It starts with av_test_. Export it, and read it into settings so nothing else touches the environment directly:

shell
export AVELTO_API_KEY=av_test_...
Python
# settings.py
import os

AVELTO_API_KEY = os.environ["AVELTO_API_KEY"]
Sandbox rules

Test keys never deliver anything; they run the pipeline and record events. The sandbox sender you@sandbox.avelto.dev only delivers to your account's verified owner email and to the simulator addresses delivered@, bounced@ and complained@sandbox.avelto.dev. Anything else is refused with 403 sandbox_recipient_not_allowed. To send to anyone, verify a domain (step 5).

3. Send your first email

A small module wraps the four calls this page uses. Every request carries the key as a bearer token; a non-2xx status carries { "error": { "code", "message" } }, which becomes an AveltoError.

Python
# myapp/avelto.py
import requests
from django.conf import settings

API = "https://api-staging.avelto.dev"


class AveltoError(Exception):
    def __init__(self, status, code, message):
        super().__init__(f"{status} {code}: {message}")
        self.status = status
        self.code = code


def request(method, path, json=None):
    r = requests.request(
        method,
        f"{API}{path}",
        headers={"Authorization": f"Bearer {settings.AVELTO_API_KEY}"},
        json=json,
    )
    if r.status_code >= 400:
        err = r.json()["error"]
        raise AveltoError(r.status_code, err["code"], err["message"])
    return r.json()


def send_email(body):
    return request("POST", "/v1/emails", json=body)


def get_email(email_id):
    return request("GET", f"/v1/emails/{email_id}")


def create_domain(name):
    return request("POST", "/v1/domains", json={"name": name})


def get_domain(domain_id):
    return request("GET", f"/v1/domains/{domain_id}")

The view sends and returns the API's answer, or the error envelope with the same status.

Python
# myapp/views.py
from django.http import JsonResponse
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_POST

from .avelto import AveltoError, send_email


@csrf_exempt  # so you can try it with curl; drop this behind a form or auth
@require_POST
def send(request):
    try:
        result = send_email({
            "from": "[email protected]",
            "to": "[email protected]",
            "subject": "Hello from Avelto",
            "text": "It works.",
        })
    except AveltoError as e:
        return JsonResponse(
            {"error": {"code": e.code, "message": str(e)}}, status=e.status
        )
    return JsonResponse(result, status=201)  # {"id": "9c1f4a52-..."}
Python
# urls.py
from django.urls import path

from myapp import views

urlpatterns = [
    path("send", views.send),
]

Run python manage.py runserver and call the view:

shell
curl -X POST http://localhost:8000/send

The API answers 201 Created with the email id, and the view returns the same:

HTTP
HTTP/1.1 201 Created
Content-Type: application/json

{ "id": "9c1f4a52-6f6e-4b8f-9b8e-2e1a5c7d3f10" }
Retrying safely

Send an Idempotency-Key header (any unique string, such as your order id) with every POST /v1/emails. If the request times out or comes back 429, 502, 503 or 504, wait a moment and send it again unchanged with the same key: the API returns the original email id instead of sending twice. See Idempotency.

4. Check the log

A second view fetches the email by id, with a second route for it. status moves from queued to sent to delivered, and events records each step: email.queued, email.sent, email.delivered.

Python
# myapp/views.py (add to the file from step 3)
from .avelto import get_email


def status(request, email_id):
    email = get_email(email_id)
    return JsonResponse({
        "status": email["status"],  # "queued", then "sent", then "delivered"
        "events": [e["type"] for e in email["events"]],
    })
Python
# urls.py (add the second route)
urlpatterns = [
    path("send", views.send),
    path("emails/<uuid:email_id>", views.status),
]
shell
curl http://localhost:8000/emails/9c1f4a52-6f6e-4b8f-9b8e-2e1a5c7d3f10

5. Verify a domain

Adding a domain is a one-off task, so it fits a management command. Publish the DNS records it prints (three DKIM CNAMEs, an SPF TXT and a DMARC TXT) at your DNS provider; the command polls GET /v1/domains/:id, which re-checks DNS on every call, until status is verified. Use a subdomain such as mail.acme.com.

Python
# myapp/management/commands/verify_domain.py
import time

from django.core.management.base import BaseCommand

from myapp.avelto import create_domain, get_domain


class Command(BaseCommand):
    help = "Add a sending domain and wait until it is verified"

    def add_arguments(self, parser):
        parser.add_argument("name")

    def handle(self, *args, **options):
        domain = create_domain(options["name"])

        for rec in domain["dns_records"]:
            self.stdout.write(
                f"{rec['type']}\t{rec['name']}\t{rec['value']}\t({rec['purpose']})"
            )

        # Publish the records, then poll. GET re-checks DNS on every call.
        status = domain["status"]
        while status == "pending":
            time.sleep(30)
            status = get_domain(domain["id"])["status"]

        self.stdout.write(status)  # "verified" or "failed"
shell
python manage.py verify_domain mail.acme.com

Once the domain is verified, switch AVELTO_API_KEY to a live key (av_live_) and change from in the view to an address on it, such as [email protected]. Nothing else changes.

Next

  • Send email: every field, attachments, tags, scheduling and idempotency.
  • Webhooks: get events pushed to your app, with a Python signature check.
  • Test mode: test keys, the sandbox sender and the simulator addresses.