eusend
Frameworks

FastAPI

Send transactional email from FastAPI through eusend with the Python SDK — in a background task so the response is not held up, with idempotent retries and typed errors.

The eusend Python SDK is synchronous and built on the standard library, so it has no HTTP client to conflict with yours. In FastAPI that means one rule: don't call it directly inside an async def endpoint, where it would block the event loop. Use a background task, or a plain def endpoint, and FastAPI runs the call in its thread pool.

Setup

pip install eusend
.env
EUSEND_API_KEY=eu_test_...

The SDK reads EUSEND_API_KEY from the environment, so there is nothing to configure in code. An eu_test_ key runs validation, quota and suppression checks and logs the email without delivering it.

Send in a background task

For email the caller does not need confirmed — a welcome message, a notification — respond first and send afterwards:

main.py
import logging

import eusend
from fastapi import BackgroundTasks, FastAPI
from pydantic import BaseModel, EmailStr

app = FastAPI()
log = logging.getLogger(__name__)


class Signup(BaseModel):
    name: str
    email: EmailStr


def send_welcome(name: str, email: str) -> None:
    try:
        eusend.Emails.send({
            'from': 'Acme <hello@yourdomain.com>',
            'to': email,
            'subject': 'Welcome to Acme',
            'html': f'<p>Hi {name}, thanks for signing up.</p>',
        })
    except eusend.EusendError as e:
        log.warning('welcome email failed: %s %s', e.code, e.message)


@app.post('/signup', status_code=201)
async def signup(body: Signup, background_tasks: BackgroundTasks):
    background_tasks.add_task(send_welcome, body.name, body.email)
    return {'ok': True}

EmailStr needs pip install "pydantic[email]". Escape anything user-supplied before it goes into html — the example above keeps it short, and html.escape(name) is the fix.

Send and report the result

When the caller does need to know — a receipt, a password reset — use a def endpoint and turn the SDK's exception into a response:

from fastapi import HTTPException


class Receipt(BaseModel):
    order_id: str
    email: EmailStr


@app.post('/receipts')
def send_receipt(body: Receipt):
    try:
        email = eusend.Emails.send(
            {
                'from': 'Acme <billing@yourdomain.com>',
                'to': body.email,
                'subject': f'Receipt for order {body.order_id}',
                'html': '<p>Thanks — your order is confirmed.</p>',
            },
            options={'idempotency_key': f'receipt-{body.order_id}'},
        )
    except eusend.EusendError as e:
        raise HTTPException(status_code=502, detail=e.code)

    return {'id': email['id']}

The idempotency_key makes a retried request return the original email instead of sending a second one. Branch on e.code — the error code — not on the message. eusend.RateLimitError is the subclass to catch if you want to back off on a 429.

Surviving a restart

A background task lives in the process: if the server restarts before it runs, the email is never sent. For mail that must go out, put the send on a real queue (Celery, RQ, Arq) and keep the idempotency_key so a retried job cannot send twice.

Next steps