Skip to content
Python & FastAPI Tutorial

How to Send Emails with Python and FastAPI

Send high-deliverability transactional emails from Python using standard HTTP requests. No proprietary SDK required—just clean, modern REST calls.

Written by Ashish Sharda•✓ Verified for Python 3.10+ & FastAPI•4 min read

💡 Summary: Sending Transactional Emails in Python

To send transactional emails in Python or FastAPI, dispatch an HTTP POST request to https://sently.in/api/v1/emails with header Authorization: Bearer YOUR_API_KEY and a JSON payload containing from, to, subject, and html using httpx or requests.

Python is a staple for backends, data pipelines, and microservices. Because Sently exposes a clean, standard REST API with predictable JSON payloads, you don't need a heavy SDK to get started. You can use standard HTTP libraries like requests or modern async libraries like httpx in FastAPI or Django.

In this tutorial, you will learn how to make synchronous HTTP requests with requests, send emails asynchronously using FastAPI's background tasks with httpx, and handle errors cleanly.

1 Install HTTP Clients

Choose your favorite HTTP client. For synchronous scripts or Django/Flask services, requests is straightforward. For FastAPI or async services, httpx is optimal:

# Install requests for synchronous scripts
pip install requests

# Or install httpx & FastAPI for asynchronous backends
pip install fastapi uvicorn httpx pydantic

2 Send Emails with requests.post()

Sending a transactional email is as simple as making an authorized POST request to https://sently.in/api/v1/emails (or your custom Sently domain endpoint):

# send_email.py
import os
import requests

SENTLY_API_KEY = os.environ.get("SENTLY_API_KEY", "snt_live_your_api_key_here")
SENTLY_BASE_URL = os.environ.get("SENTLY_BASE_URL", "https://sently.in")

def send_transactional_email(to_email: str, recipient_name: str, code: str):
    endpoint = f"{SENTLY_BASE_URL}/api/v1/emails"
    headers = {
        "Authorization": f"Bearer {SENTLY_API_KEY}",
        "Content-Type": "application/json",
    }

    payload = {
        "from": "Acme Security <[email protected]>",
        "to": to_email,
        "subject": "Your Two-Factor Verification Code",
        "html": f"""
            <div style="font-family: sans-serif; max-width: 500px; margin: auto; padding: 20px;">
                <h2>Security Verification</h2>
                <p>Hello {recipient_name},</p>
                <p>Use the following verification code to access your account:</p>
                <div style="font-size: 28px; font-weight: bold; letter-spacing: 4px; padding: 14px 20px; background: #f3f3f3; display: inline-block; border-radius: 6px;">
                    {code}
                </div>
                <p style="margin-top: 20px; font-size: 13px; color: #666;">This code expires in 10 minutes. If you did not request this, please change your password.</p>
            </div>
        """,
        "text": f"Hello {recipient_name},\n\nYour verification code is: {code}\nThis code expires in 10 minutes.",
        "category": "transactional",
        "metadata": {
            "type": "2fa-login",
            "recipient": to_email,
        },
    }

    try:
        response = requests.post(endpoint, json=payload, headers=headers, timeout=15)

        # Check for errors
        if response.status_code in (200, 201):
            data = response.json()
            print(f"Email sent successfully! ID: {data['id']}, Status: {data['status']}")
            return data
        else:
            error_info = response.json()
            print(f"Failed to send email [{response.status_code}]: {error_info}")
            return None

    except requests.exceptions.Timeout:
        print("Network request to Sently timed out.")
        raise
    except requests.exceptions.RequestException as exc:
        print(f"Network error communicating with Sently: {exc}")
        raise

# Example execution
if __name__ == "__main__":
    send_transactional_email("[email protected]", "Alex", "839201")

3 Async Sending in FastAPI with httpx

In FastAPI applications, you want to avoid blocking the event loop. Combining httpx.AsyncClient with FastAPI's BackgroundTasks allows your API to return an immediate 200 response to users while Sently delivers the email in the background:

# main.py
import os
from fastapi import FastAPI, HTTPException, BackgroundTasks
from pydantic import BaseModel, EmailStr
import httpx

app = FastAPI(title="Sently Email Service")

SENTLY_API_KEY = os.environ.get("SENTLY_API_KEY", "snt_live_your_api_key_here")
SENTLY_BASE_URL = os.environ.get("SENTLY_BASE_URL", "https://sently.in")

class PasswordResetRequest(BaseModel):
    email: EmailStr
    user_name: str
    reset_token: str

async def dispatch_sently_email(payload: dict, idempotency_key: str | None = None) -> dict:
    """Async helper to dispatch email via Sently's REST API."""
    headers = {
        "Authorization": f"Bearer {SENTLY_API_KEY}",
        "Content-Type": "application/json",
    }
    if idempotency_key:
        headers["Idempotency-Key"] = idempotency_key

    async with httpx.AsyncClient(timeout=10.0) as client:
        try:
            response = await client.post(
                f"{SENTLY_BASE_URL}/api/v1/emails",
                headers=headers,
                json=payload,
            )

            if response.status_code not in (200, 201):
                err = response.json().get("error", {})
                raise HTTPException(
                    status_code=response.status_code,
                    detail=f"Sently API Error ({err.get('code', 'unknown')}): {err.get('message', response.text)}",
                )

            return response.json()

        except httpx.TimeoutException:
            raise HTTPException(status_code=504, detail="Email service timed out")
        except httpx.RequestError as exc:
            raise HTTPException(status_code=502, detail=f"Failed to reach email gateway: {str(exc)}")

@app.post("/api/auth/reset-password")
async def request_password_reset(request: PasswordResetRequest, background_tasks: BackgroundTasks):
    reset_link = f"https://yourdomain.com/reset?token={request.reset_token}"

    payload = {
        "from": "Acme Auth <[email protected]>",
        "to": request.email,
        "subject": "Reset your password",
        "html": f"""
            <p>Hi {request.user_name},</p>
            <p>You requested a password reset. Click the link below to set a new password:</p>
            <p><a href="{reset_link}" style="padding: 10px 18px; background: #1d1b16; color: #fff; text-decoration: none; border-radius: 4px;">Reset Password</a></p>
            <p style="color: #888; font-size: 12px;">This link is valid for 1 hour.</p>
        """,
        "category": "transactional",
        "metadata": {"token": request.reset_token},
    }

    # Offload email dispatch to FastAPI BackgroundTasks for non-blocking latency
    background_tasks.add_task(
        dispatch_sently_email,
        payload,
        idempotency_key=f"reset-{request.reset_token}",
    )

    return {"message": "Password reset instructions dispatched"}

Idempotency Support: Pass an Idempotency-Key header with your POST request. If a client retries due to a network glitch, Sently recognizes the duplicate key within 24 hours and guarantees you will never send the customer duplicate emails.

4 Handling API Errors Gracefully

Sently returns standard HTTP status codes along with descriptive JSON error objects containing code, message, and fields:

import requests

def send_with_structured_error_handling(to_email: str, subject: str, html_body: str):
    headers = {
        "Authorization": f"Bearer {SENTLY_API_KEY}",
        "Content-Type": "application/json",
    }
    payload = {
        "from": "Acme <[email protected]>",
        "to": to_email,
        "subject": subject,
        "html": html_body,
    }

    response = requests.post(
        f"{SENTLY_BASE_URL}/api/v1/emails",
        json=payload,
        headers=headers,
        timeout=10,
    )

    if response.status_code == 200:
        return response.json()

    # Sently returns structured JSON errors
    data = response.json()
    error = data.get("error", {})
    error_code = error.get("code")
    message = error.get("message")
    fields = error.get("fields", {})

    if response.status_code == 400:
        print(f"Validation Error ({error_code}): {message}")
        for field, detail in fields.items():
            print(f"  -> Field '{field}': {detail}")
    elif response.status_code == 401:
        print("Unauthorized: Check that your SENTLY_API_KEY is active and valid.")
    elif response.status_code == 429:
        print("Rate limit reached. Back off and retry.")
    else:
        print(f"Server error [{response.status_code}]: {message}")

    return None

5 Production Checklist

  • Domain DNS Records: Confirm your sending domain has active SPF, DKIM, and DMARC records configured in the Sently dashboard.
  • Timeout Configuration: Always set explicit timeouts on HTTP calls (e.g. 10 to 15 seconds) to ensure slow networks never hang worker threads.
  • Environment Security: Keep SENTLY_API_KEY in environment variables or a secure secrets manager (such as Doppler, HashiCorp Vault, or your cloud provider's secret store).

Ready to power your Python emails with Sently?

Create your Sently developer account today and start sending transactional emails with complete reliability.