Sendkit TeamHow to send email with Django using the Sendkit API
Integrate Sendkit into your Django app with the Python SDK. Covers configuration, sending, templates, async tasks, and error handling.

Django's built-in django.core.mail module works, but it's tied to SMTP. You configure EMAIL_HOST, hope the connection doesn't time out mid-request, and get no structured feedback when something goes wrong. For transactional email in a production Django app, an API is a better foundation.
This guide covers integrating the Sendkit email API into a Django project using the Python SDK. We'll go from installation through templates, async sending with Celery, webhooks, and the SMTP fallback for legacy code.
Install the SDK
pip install sendkitAdd it to your requirements.txt or pyproject.toml. The package has no heavy dependencies, so it won't bloat your Django project.
Configure your API key
Store your API key as an environment variable. Never hardcode it in settings.py.
# settings.py
import os
SENDKIT_API_KEY = os.environ.get("SENDKIT_API_KEY")Then create a reusable client. I put this in a small utility module:
# emails/client.py
from django.conf import settings
from sendkit import Sendkit
client = Sendkit(settings.SENDKIT_API_KEY)If you prefer, Sendkit() with no arguments reads the SENDKIT_API_KEY environment variable automatically. Either approach works. The explicit settings.py route is better when you want Django's configuration to be the single source of truth.
Grab an API key from your Sendkit dashboard. Keys starting with sk_test_ validate everything but don't deliver, which is what you want during development.
Send a basic email from a Django view
With the client module in place, sending from any Django view is straightforward. Here's a contact form handler that sends an email on submission and returns the message ID:
# views.py
from django.http import JsonResponse
from django.views.decorators.http import require_POST
from sendkit import SendkitError
from .client import client
@require_POST
def contact_form(request):
name = request.POST.get("name")
email = request.POST.get("email")
message = request.POST.get("message")
try:
result = client.emails.send(
from_="[email protected]",
to="[email protected]",
subject=f"Contact form: {name}",
text=f"From: {name} ({email})\n\n{message}",
)
return JsonResponse({"status": "sent", "id": result["id"]})
except SendkitError as e:
return JsonResponse({"status": "error", "message": e.message}, status=500)The from_ address must be on a domain you've verified in Sendkit. The trailing underscore exists because from is a reserved keyword in Python.

Use Django templates for HTML emails
Inline HTML strings in Python code get messy fast. Django's template engine handles this cleanly with render_to_string.
Create a template:
<!-- templates/emails/welcome.html -->
<div style="font-family: sans-serif; max-width: 600px; margin: 0 auto;">
<h2>Welcome, {{ user.first_name }}</h2>
<p>Your account is ready. Here's what to do next:</p>
<ul>
<li>Complete your profile</li>
<li>Connect your first integration</li>
<li>Invite your team</li>
</ul>
<a href="{{ dashboard_url }}"
style="background: #0066ff; color: white; padding: 12px 24px;
text-decoration: none; border-radius: 4px;
display: inline-block; margin-top: 16px;">
Go to dashboard
</a>
</div>Then render and send:
# emails/transactional.py
from django.template.loader import render_to_string
from sendkit import SendkitError
from .client import client
def send_welcome_email(user):
html = render_to_string("emails/welcome.html", {
"user": user,
"dashboard_url": "https://app.yoursite.com/dashboard",
})
try:
result = client.emails.send(
from_="[email protected]",
to=user.email,
subject=f"Welcome, {user.first_name}",
html=html,
text=f"Welcome, {user.first_name}. Your account is ready.",
)
return result["id"]
except SendkitError as e:
print(f"Failed to send welcome email: {e.name} - {e.message}")
raiseAlways pass both html and text. Some mail clients can't render HTML, and a plain text fallback ensures everyone sees something readable. Use inline CSS in your HTML templates — email clients strip <style> blocks.
Send async with Celery
Sending email inside a request-response cycle adds latency you don't need. If the Sendkit API takes 200ms to respond, that's 200ms your user stares at a spinner. For anything that isn't a blocking flow (like "click here to verify your email"), push the send to a background task.
Celery is the standard choice in Django. Here's a task that sends an email and retries on transient failures:
# emails/tasks.py
from celery import shared_task
from sendkit import Sendkit, SendkitError
client = Sendkit() # reads SENDKIT_API_KEY from env
@shared_task(bind=True, max_retries=3, default_retry_delay=30)
def send_email_task(self, from_, to, subject, html=None, text=None):
try:
result = client.emails.send(
from_=from_,
to=to,
subject=subject,
html=html,
text=text,
)
return result["id"]
except SendkitError as e:
if e.status_code in (429, 500, 502, 503):
raise self.retry(exc=e)
raise
except Exception as e:
raise self.retry(exc=e)Call it from your view:
from .tasks import send_email_task
send_email_task.delay(
from_="[email protected]",
to=user.email,
subject="Welcome aboard",
html=rendered_html,
text="Welcome. Your account is ready.",
)The task retries up to three times on rate limits and server errors. Validation errors (422) fail immediately since retrying won't fix bad input. This is the pattern you want for any email that isn't blocking user flow.
If you're not using Celery, Django-Q2 or Huey work the same way. The key point is: don't make the user wait for an HTTP round-trip to an external API when you can fire and forget.
Error handling with SendkitError
SendkitError gives you structured error data instead of generic exceptions:
from sendkit import Sendkit, SendkitError
client = Sendkit()
try:
result = client.emails.send(
from_="[email protected]",
to="[email protected]",
subject="Hello",
html="<h1>Welcome!</h1>",
)
print(result["id"])
except SendkitError as e:
print(e.name, e.message, e.status_code)In a Django context, map these to appropriate HTTP responses:
from django.http import JsonResponse
from sendkit import SendkitError
def handle_sendkit_error(e):
if e.status_code == 422:
return JsonResponse({"error": e.message}, status=400)
if e.status_code == 429:
return JsonResponse({"error": "Too many requests"}, status=429)
return JsonResponse({"error": "Email delivery failed"}, status=502)Don't expose internal error details to end users. Log the full error server-side, return something generic to the client.

Webhook handling
Sendkit sends webhooks for delivery events — bounces, complaints, opens, clicks. You need a Django view that accepts POST requests and verifies the HMAC-SHA256 signature.
# views.py
import hashlib
import hmac
import json
from django.conf import settings
from django.http import HttpResponse, HttpResponseForbidden
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_POST
@csrf_exempt
@require_POST
def sendkit_webhook(request):
signature = request.headers.get("X-Sendkit-Signature", "")
payload = request.body
expected = hmac.new(
settings.SENDKIT_WEBHOOK_SECRET.encode(),
payload,
hashlib.sha256,
).hexdigest()
if not hmac.compare_digest(signature, expected):
return HttpResponseForbidden("Invalid signature")
event = json.loads(payload)
event_type = event.get("type")
if event_type == "email.bounced":
handle_bounce(event["data"]["recipient"])
elif event_type == "email.complained":
handle_complaint(event["data"]["recipient"])
return HttpResponse(status=200)Add the URL to your urls.py:
path("webhooks/sendkit/", views.sendkit_webhook, name="sendkit-webhook"),Store SENDKIT_WEBHOOK_SECRET in your settings.py alongside the API key. Always verify the signature — without it, anyone can POST fake events to your endpoint. Use @csrf_exempt because Sendkit's webhook requests won't carry a Django CSRF token, and that's expected.
In production, you'll want to log every webhook event and process them idempotently. Sendkit may retry delivery if your endpoint doesn't return a 2xx, so the same event can arrive more than once. Use the event ID to deduplicate.
SMTP alternative
If your Django project already uses django.core.mail heavily and you don't want to rewrite everything, point the email backend at Sendkit's SMTP relay:
# settings.py
EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend"
EMAIL_HOST = "smtp.sendkit.com"
EMAIL_PORT = 587
EMAIL_USE_TLS = True
EMAIL_HOST_USER = "sendkit"
EMAIL_HOST_PASSWORD = "sk_live_your_api_key"
DEFAULT_FROM_EMAIL = "[email protected]"Then send_mail works as usual:
from django.core.mail import send_mail
send_mail(
"Order confirmed",
"Your order #4821 has been confirmed.",
"[email protected]",
["[email protected]"],
)Your API key doubles as your SMTP password. The API is still the better choice for new code — no SMTP handshake overhead, immediate message IDs in the response, and structured errors instead of SMTP reply codes. But SMTP is a valid path when you need backward compatibility.
Check pricing for rate limits and volume tiers on both API and SMTP.
What comes next
The sending code is the easy part. Getting it into production reliably takes a bit more thought, but not much if you follow the patterns above: use the SDK for new code, push sends to Celery, handle errors properly, and verify webhook signatures.
Deliverability is the ongoing work. Authenticate your domain with DKIM, SPF, and DMARC — without these, Gmail and Outlook will either spam-folder your messages or reject them outright. Watch your bounce rate. Sending to dead addresses tanks your sender reputation, and once that's damaged, it affects every email from your domain. Validate addresses before sending to catch typos and disposable mailboxes.
For the broader Python setup without Django-specific patterns, see the Python email sending guide. If your deliverability needs work, the improve email deliverability guide covers sender reputation, authentication records, and list hygiene in detail.
The Sendkit docs cover templates, batch sending, and the full webhook event reference.
Share this article