# Send email with Python

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

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](/docs/domains/add-a-domain/), for example `acme.com`.
- An [API key](/docs/developers/api-keys/). A Sending Only key restricted to your domain is enough.
- Until your workspace has [production access](/docs/workspaces/production-access/), you can only send to the account emails of workspace members.

## Install the SDK

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

```bash
```

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

```python title="send.py"

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](/docs/api-reference/emails/send/).

### 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`:

```python title="settings.py"

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`:

```python title="send_smtp.py"
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](/docs/smtp/settings/).

## Receive webhooks

[Create a webhook](/docs/webhooks/set-up/) and store its signing secret in `EMAILIT_WEBHOOK_SECRET`. Verify the signature against the raw request body with this helper:

```python title="emailit_webhooks.py"

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:

**Flask**

```python title="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
```

**Django**

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

from django.http import HttpResponse, HttpResponseForbidden
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_POST

from emailit_webhooks import is_valid_signature

@csrf_exempt
@require_POST
def emailit_webhook(request):
    if not is_valid_signature(
        request.body,
        request.headers.get("X-Emailit-Signature", ""),
        request.headers.get("X-Emailit-Timestamp", ""),
    ):
        return HttpResponseForbidden("Invalid signature")

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

    return HttpResponse(status=200)
```

Return a `2xx` within 30 seconds; other responses are retried. See [Request signature](/docs/webhooks/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](/docs/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](/docs/email-api/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

  - [Send email with the API](/docs/email-api/send-email/): Attachments, scheduling and tracking.
  - [Templates](/docs/templates/): Design emails in Emailit and send them by alias.
  - [Webhook event types](/docs/webhooks/event-types/): Every event and its payload.
  - [SDKs and libraries](/docs/sdks/): All official libraries.

---
Source: https://emailit.com/docs/frameworks/python/
