How to Send Emails in FastAPI (2026 Guide)

Nik@nikpolale
11 min read
FastAPI is async by default. Email sending fits naturally into its async model with httpx for non-blocking HTTP calls. One request to an API provider, they handle delivery and retries.
This guide covers async email sending, background tasks, and Jinja2 templates.
Install
Terminal
pip install fastapi httpx uvicorn jinja2Terminal
pip install fastapi resend uvicorn jinja2Terminal
pip install fastapi sendgrid uvicorn jinja2Create an Email Client
email_client.py
import os
import httpx
SEQUENZY_API_KEY = os.environ["SEQUENZY_API_KEY"]
BASE_URL = "https://api.sequenzy.com/v1"
client = httpx.AsyncClient(
base_url=BASE_URL,
headers={"Authorization": f"Bearer {SEQUENZY_API_KEY}"},
)
async def send_email(to: str, subject: str, body: str) -> dict:
response = await client.post(
"/transactional/send",
json={"to": to, "subject": subject, "body": body},
)
response.raise_for_status()
return response.json()email_client.py
import os
import resend
resend.api_key = os.environ["RESEND_API_KEY"]
async def send_email(to: str, subject: str, html: str) -> dict:
# resend SDK is sync, wrap if needed
return resend.Emails.send({
"from": "Your App <noreply@yourdomain.com>",
"to": to,
"subject": subject,
"html": html,
})email_client.py
import os
from sendgrid import SendGridAPIClient
from sendgrid.helpers.mail import Mail
sg = SendGridAPIClient(os.environ["SENDGRID_API_KEY"])
async def send_email(to: str, subject: str, html: str):
message = Mail(
from_email="noreply@yourdomain.com",
to_emails=to,
subject=subject,
html_content=html,
)
return sg.send(message)Send from an Endpoint
main.py
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel, EmailStr
from email_client import send_email
app = FastAPI()
class WelcomeRequest(BaseModel):
email: EmailStr
name: str
@app.post("/api/send-welcome")
async def send_welcome(req: WelcomeRequest):
result = await send_email(
to=req.email,
subject=f"Welcome, {req.name}",
body=f"<h1>Welcome, {req.name}</h1><p>Your account is ready.</p>",
)
return {"jobId": result["jobId"]}main.py
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel, EmailStr
from email_client import send_email
app = FastAPI()
class WelcomeRequest(BaseModel):
email: EmailStr
name: str
@app.post("/api/send-welcome")
async def send_welcome(req: WelcomeRequest):
result = await send_email(
to=req.email,
subject=f"Welcome, {req.name}",
html=f"<h1>Welcome, {req.name}</h1><p>Your account is ready.</p>",
)
return {"id": result["id"]}main.py
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel, EmailStr
from email_client import send_email
app = FastAPI()
class WelcomeRequest(BaseModel):
email: EmailStr
name: str
@app.post("/api/send-welcome")
async def send_welcome(req: WelcomeRequest):
await send_email(
to=req.email,
subject=f"Welcome, {req.name}",
html=f"<h1>Welcome, {req.name}</h1><p>Your account is ready.</p>",
)
return {"sent": True}Background Tasks
FastAPI has built-in BackgroundTasks for fire-and-forget operations:
from fastapi import BackgroundTasks
async def send_in_background(to: str, subject: str, body: str):
await send_email(to=to, subject=subject, body=body)
@app.post("/api/signup")
async def signup(req: WelcomeRequest, background_tasks: BackgroundTasks):
# Create user first...
background_tasks.add_task(
send_in_background,
to=req.email,
subject=f"Welcome, {req.name}",
body=f"<h1>Welcome, {req.name}</h1><p>Your account is ready.</p>",
)
return {"message": "Account created"}The response returns immediately. The email sends after.
Jinja2 Templates
from jinja2 import Environment, FileSystemLoader
templates = Environment(loader=FileSystemLoader("templates/emails"))
def render_email(template_name: str, **kwargs) -> str:
template = templates.get_template(template_name)
return template.render(**kwargs)
# templates/emails/welcome.html
# <h1>Welcome, {{ name }}</h1>
# <p>Your account is ready.</p>
# Usage:
html = render_email("welcome.html", name="Alice")
await send_email(to=email, subject="Welcome", body=html)Going to Production
1. Verify Your Domain
Add SPF, DKIM, DMARC DNS records.
2. Use Environment Variables
export SEQUENZY_API_KEY=sq_your_key3. Connection Pooling
httpx.AsyncClient reuses connections by default. Create one client and share it across the app.
Beyond Transactional
Sequenzy handles transactional sends, marketing campaigns, automated sequences, and subscriber management from one API. Native Stripe integration for SaaS.
Wrapping Up
- Async email sending with
httpx - Pydantic validation for type-safe request handling
- BackgroundTasks for non-blocking sends
- Jinja2 templates for email HTML
Pick your provider, copy the patterns, and start sending.