Back to Blog

How to Send Emails in FastAPI (2026 Guide)

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 jinja2
Terminal
pip install fastapi resend uvicorn jinja2
Terminal
pip install fastapi sendgrid uvicorn jinja2

Create 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_key

3. 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

  1. Async email sending with httpx
  2. Pydantic validation for type-safe request handling
  3. BackgroundTasks for non-blocking sends
  4. Jinja2 templates for email HTML

Pick your provider, copy the patterns, and start sending.