# 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**

```bash
pip install requests
```

## 2. Create an API key

[Sign in](/login), 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**

```bash
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": "you@sandbox.avelto.dev",
            "to": "delivered@sandbox.avelto.dev",
            "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**

```bash
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](/docs/send-email).

## 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**

```bash
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**

```bash
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 `hello@mail.acme.com`. Nothing else changes.

## Next

- [Send email](/docs/send-email): every field, attachments, tags, scheduling and idempotency.
- [Webhooks](/docs/webhooks): get events pushed to your app, with a Python signature check.
- [Test mode](/docs/test-mode): test keys, the sandbox sender and the simulator addresses.

---

Rendered page: https://staging.avelto.dev/docs/quickstart/django
