Skip to content
Docs

Tutorial

Send email from Python with the emailit SDK, Django's SMTP backend or smtplib, and verify Emailit webhooks in Flask or Django.

Updated Oct 1, 2026

This guide shows how to send email from Python with the official emailit package, how to use SMTP from Django or the standard library instead, and how to verify webhook signatures in Flask and Django.

Prerequisites

  • Python 3.7 or later.
  • A verified sending domain, for example acme.com.
  • An API key. A Sending Only key restricted to your domain is enough.
  • Until your workspace has production access, you can only send to the account emails of workspace members.

Install the SDK

Terminal
pip install emailit

The SDK uses requests under the hood.

Configure your API key

Set the key in the environment of your app server, worker or container:

Terminal
export EMAILIT_API_KEY=secret_••••••••••••••••••••••••••••••••

Read it with os.environ["EMAILIT_API_KEY"], so a missing key fails loudly at startup instead of at the first send.

Send an email

send.py
import os

from emailit import EmailitClient

client = EmailitClient(os.environ["EMAILIT_API_KEY"])

email = client.emails.send({
    "from": "Acme <hello@acme.com>",
    "to": "ada@example.com",
    "subject": "Reset your password",
    "html": "<p>Use this link to reset your password.</p>",
    "text": "Use this link to reset your password.",
})

print(email.id)  # em_…

Create the client once and reuse it. The same send call accepts cc, bcc, reply_to, attachments, template with variables, scheduled_at and tracking; see Send an email.

Handle errors

Python
from emailit import (
    ApiErrorException,
    AuthenticationException,
    RateLimitException,
    UnprocessableEntityException,
)

try:
    client.emails.send(message)
except RateLimitException:
    # 429: wait and retry, for example from a task queue
    raise
except AuthenticationException:
    # 401: the API key is missing or invalid
    raise
except UnprocessableEntityException as e:
    # 422: for example, the from domain isn't verified
    print(e.json_body)
except ApiErrorException as e:
    print(e.http_status, e.json_body)

The SDK is synchronous. In async frameworks such as FastAPI, call it from a regular def endpoint or a background task so it doesn’t block the event loop.

Send with SMTP instead

Django

Django’s built-in SMTP backend works with Emailit. Add this to settings.py:

settings.py
import os

EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend"
EMAIL_HOST = "smtp.emailit.com"
EMAIL_PORT = 587
EMAIL_USE_TLS = True
EMAIL_HOST_USER = "emailit"
EMAIL_HOST_PASSWORD = os.environ["EMAILIT_API_KEY"]
DEFAULT_FROM_EMAIL = "Acme <hello@acme.com>"
SERVER_EMAIL = "alerts@acme.com"

Then use Django’s mail functions as usual:

Python
from django.core.mail import send_mail

send_mail(
    subject="Reset your password",
    message="Use this link to reset your password.",
    from_email=None,  # uses DEFAULT_FROM_EMAIL
    recipient_list=["ada@example.com"],
    html_message="<p>Use this link to reset your password.</p>",
)

DEFAULT_FROM_EMAIL and SERVER_EMAIL must use a verified sending domain. For implicit TLS on port 465, set EMAIL_PORT = 465 and EMAIL_USE_SSL = True instead of EMAIL_USE_TLS.

Standard library

Without a framework, use smtplib:

send_smtp.py
import os
import smtplib
from email.message import EmailMessage

message = EmailMessage()
message["From"] = "Acme <hello@acme.com>"
message["To"] = "ada@example.com"
message["Subject"] = "Reset your password"
message.set_content("Use this link to reset your password.")
message.add_alternative("<p>Use this link to reset your password.</p>", subtype="html")

with smtplib.SMTP("smtp.emailit.com", 587) as smtp:
    smtp.starttls()
    smtp.login("emailit", os.environ["EMAILIT_API_KEY"])
    smtp.send_message(message)

If your network blocks 587, use port 2525 or 2587 with the same code. See SMTP settings.

Receive webhooks

Create a webhook and store its signing secret in EMAILIT_WEBHOOK_SECRET. Verify the signature against the raw request body with this helper:

emailit_webhooks.py
import hashlib
import hmac
import os
import time


def is_valid_signature(raw_body: bytes, signature: str, timestamp: str) -> bool:
    if not signature or not timestamp or not timestamp.isdigit():
        return False
    if abs(time.time() - int(timestamp)) > 300:
        return False
    secret = os.environ["EMAILIT_WEBHOOK_SECRET"].encode()
    expected = hmac.new(secret, timestamp.encode() + b"." + raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, signature)

Then call it from your framework’s route:

app.py
import json

from flask import Flask, abort, request

from emailit_webhooks import is_valid_signature

app = Flask(__name__)


@app.post("/webhooks/emailit")
def emailit_webhook():
    raw_body = request.get_data()
    if not is_valid_signature(
        raw_body,
        request.headers.get("X-Emailit-Signature", ""),
        request.headers.get("X-Emailit-Timestamp", ""),
    ):
        abort(401)

    for event in json.loads(raw_body):  # an array of up to 100 events
        if event["type"] == "email.bounced":
            address = event["data"]["object"]["to"]
            # Stop emailing this address.

    return "", 200

Return a 2xx within 30 seconds; other responses are retried. See Request signature.

Production tips

  • Send from a task queue. Use Celery, RQ or your framework’s background tasks for bulk mail, and throttle workers to stay under your sending limits (2 emails per second by default).
  • Make retries safe. When a task retries after a timeout, send an Idempotency-Key header (with requests or httpx) so the email isn’t sent twice. See Idempotency.
  • Deduplicate webhooks. Store each event_id you process; failed deliveries are retried and can arrive more than once.
  • Use one key per environment and load it from the environment or a secrets manager. Never hard-code it in settings.py.

Next steps

Attachments, scheduling and tracking.
Design emails in Emailit and send them by alias.
Every event and its payload.
All official libraries.

Was this page helpful?

Thanks for the feedback.

Thanks, we read every message.