# Send email from Django

> Send email with Couryo from Django with send_mail and EmailMultiAlternatives, through the Python SDK's backend or over SMTP, and receive signed webhooks.

Source: https://couryo.com/en/docs/django

`send_mail`, `EmailMessage` and Django's own emails (password reset from `django.contrib.auth`, error reports) go out through Couryo by changing only the backend.

## Option 1: SDK backend

```bash title="Terminal"
pip install "couryo[django]"
```

On Django 6.1 or newer, the backend goes in `MAILERS`:

```python title="settings.py"
import os

MAILERS = {
    "default": {
        "BACKEND": "couryo.django.EmailBackend",
        "OPTIONS": {"api_key": os.environ["COURYO_API_KEY"]},  # ck_test_... while testing
    },
}
DEFAULT_FROM_EMAIL = "Example <hello@example.com>"
SERVER_EMAIL = "alerts@example.com"
```

On Django 6.0 or older, use `EMAIL_BACKEND` (Django 6.1 still accepts it with a deprecation warning, but not together with `MAILERS`):

```python title="settings.py"
import os

EMAIL_BACKEND = "couryo.django.EmailBackend"
COURYO_API_KEY = os.environ["COURYO_API_KEY"]  # ck_test_... while testing
DEFAULT_FROM_EMAIL = "Example <hello@example.com>"
SERVER_EMAIL = "alerts@example.com"
```

Your existing code does not change:

```python title="orders/emails.py"
from django.core.mail import EmailMultiAlternatives, send_mail
from django.template.loader import render_to_string

send_mail("Order received", "Thanks for your order!", None, ["ana@example.com"])

context = {"order": order}
msg = EmailMultiAlternatives(
    subject=f"Order {order.number} shipped",
    body=render_to_string("emails/order_shipped.txt", context),
    to=[order.customer.email],
    headers={"X-Couryo-Tags": "kind=order"},
)
msg.attach_alternative(render_to_string("emails/order_shipped.html", context), "text/html")
msg.send()
```

Attachments (`msg.attach(...)`) and `reply_to`, `cc` and `bcc` go through to the API. The `X-Couryo-Stream: marketing` header sends on the marketing stream; without it, the email is transactional.

### Call the API directly

```python title="accounts/views.py"
from couryo import Couryo

couryo = Couryo(language="en")  # reads COURYO_API_KEY

couryo.events.track(
    {"contact": {"email": user.email, "attributes": {"name": user.first_name}}, "name": "user.signed_up"},
    idempotency_key=f"signup-{user.pk}",
)
```

In async views, use `AsyncCouryo` with `await`.

## Option 2: SMTP

On Django 6.1 or newer:

```python title="settings.py"
MAILERS = {
    "default": {
        "BACKEND": "django.core.mail.backends.smtp.EmailBackend",
        "OPTIONS": {
            "host": "smtp.couryo.com",
            "port": 587,
            "use_tls": True,  # STARTTLS; for port 465, use "use_ssl": True instead
            "username": "couryo",
            "password": os.environ["COURYO_API_KEY"],
        },
    },
}
DEFAULT_FROM_EMAIL = "Example <hello@example.com>"
```

On Django 6.0 or older:

```python title="settings.py"
EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend"
EMAIL_HOST = "smtp.couryo.com"
EMAIL_PORT = 587
EMAIL_USE_TLS = True  # STARTTLS; for port 465, use EMAIL_USE_SSL = True
EMAIL_HOST_USER = "couryo"
EMAIL_HOST_PASSWORD = os.environ["COURYO_API_KEY"]
DEFAULT_FROM_EMAIL = "Example <hello@example.com>"
```

`use_tls` and `use_ssl` (`EMAIL_USE_TLS` and `EMAIL_USE_SSL`) cannot both be `True`. Details and reply codes in [SMTP](https://couryo.com/en/docs/smtp.md).

## Receive webhooks

```python title="webhooks/views.py"
import os

from couryo import WebhookVerificationError, verify_webhook
from django.http import HttpResponse, HttpResponseBadRequest
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_POST


@csrf_exempt
@require_POST
def couryo_webhook(request):
    try:
        event = verify_webhook(os.environ["COURYO_WEBHOOK_SECRET"], request.headers, request.body)
    except WebhookVerificationError:
        return HttpResponseBadRequest("invalid signature")

    if event["type"] == "bounced":
        ...  # event["data"]["recipient"], event["data"]["explanation"]
    return HttpResponse(status=204)
```

`request.body` is the raw body, exactly what the signature expects. The view skips CSRF because the signature already protects it.

## Django's password reset

`PasswordResetView` uses the default mailer (`MAILERS["default"]`, or `EMAIL_BACKEND` on Django 6.0 and older), so it already goes through Couryo. Just check `DEFAULT_FROM_EMAIL` (on a [verified domain](https://couryo.com/en/docs/domains.md)) and the link's domain: in production Django builds the link from the request host, so keep `ALLOWED_HOSTS` right. If the reset email does not arrive, the guide [Password reset email not arriving](https://couryo.com/en/guides/password-reset-email-not-arriving.md) walks through the causes one by one.

## Common problems

| Symptom | What to do |
|---|---|
| `InvalidRequestError` with `code` `domain_not_verified` | `DEFAULT_FROM_EMAIL` must be on a domain verified in the project |
| Nothing happens and no error | `fail_silently=True` hides errors; remove it to see Couryo's message |
| `SMTPAuthenticationError (535)` | username other than `couryo`, or a key that is invalid, revoked or without the `send` scope |
