Developer guideIntegrations
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.
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#
pip install "couryo[django]"On Django 6.1 or newer, the backend goes in MAILERS:
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):
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:
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#
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:
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:
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.
Receive webhooks#
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) 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 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 |