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.
💡 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 pydantic2 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 None5 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_KEYin environment variables or a secure secrets manager (such as Doppler, HashiCorp Vault, or your cloud provider's secret store).