This guide covers advanced Python SDK features. If you’re new to the SDK, start with the Python Quickstart .
Advanced Features
Multiple Recipients
Add multiple To, CC, and BCC recipients:
response = client.emails.send(
from_email = "notifications@yourdomain.com" ,
to = [ "user1@example.com" , "user2@example.com" ],
cc = [ "cc@example.com" , "cc2@example.com" ],
bcc = [ "bcc@example.com" ],
subject = "Team notification" ,
html = "<p>This email has multiple recipients.</p>" ,
)
BCC recipients are hidden from all other recipients. They receive the email but their addresses are not visible in the headers.
Reply-To Address
Specify a different reply-to address:
response = client.emails.send(
from_email = "notifications@yourdomain.com" ,
reply_to = "support@yourdomain.com" ,
to = [ "user@example.com" ],
subject = "Support notification" ,
html = "<p>Click reply to contact our support team.</p>" ,
)
Attachments
Add file attachments to your emails:
import base64
# Read file and encode to base64
with open ( "invoice.pdf" , "rb" ) as f:
file_data = f.read()
encoded_content = base64.b64encode(file_data).decode( "utf-8" )
response = client.emails.send(
from_email = "billing@yourdomain.com" ,
to = [ "customer@example.com" ],
subject = "Your invoice" ,
html = "<p>Please find your invoice attached.</p>" ,
attachments = [
{
"filename" : "invoice.pdf" ,
"content" : encoded_content,
"type" : "application/pdf" ,
}
],
)
Attach multiple files:
import base64
def encode_file ( filepath ):
with open (filepath, "rb" ) as f:
return base64.b64encode(f.read()).decode( "utf-8" )
attachments = [
{
"filename" : "document.pdf" ,
"content" : encode_file( "document.pdf" ),
"type" : "application/pdf" ,
},
{
"filename" : "logo.png" ,
"content" : encode_file( "logo.png" ),
"type" : "image/png" ,
},
]
response = client.emails.send(
from_email = "sender@yourdomain.com" ,
to = [ "recipient@example.com" ],
subject = "Files attached" ,
html = "<p>See attached files.</p>" ,
attachments = attachments,
)
Attachments must be base64-encoded. The total size of all attachments should not exceed 10MB. Larger files should be hosted and linked instead.
Templates
Send emails using Lettr-managed templates:
response = client.emails.send(
from_email = "notifications@yourdomain.com" ,
to = [ "user@example.com" ],
template_id = "welcome-email" ,
merge_tags = {
"name" : "John Doe" ,
"company" : "Acme Corp" ,
"verify_url" : "https://example.com/verify/abc123" ,
"support_email" : "support@yourdomain.com" ,
},
)
Templates are managed in the Lettr dashboard . Use merge tags to personalize content without rebuilding HTML in your code.
Add custom email headers:
response = client.emails.send(
from_email = "notifications@yourdomain.com" ,
to = [ "user@example.com" ],
subject = "Custom headers example" ,
html = "<p>This email has custom headers.</p>" ,
headers = {
"X-Campaign-ID" : "summer-2024" ,
"X-Priority" : "high" ,
"X-Mailer" : "Acme Mailer v1.0" ,
},
)
Tracking
Enable open and click tracking:
response = client.emails.send(
from_email = "marketing@yourdomain.com" ,
to = [ "user@example.com" ],
subject = "Newsletter" ,
html = "<p>Check out <a href='https://example.com'>our website</a>!</p>" ,
options = lettr.EmailOptions(
click_tracking = True ,
open_tracking = True ,
),
)
Open tracking works by embedding a transparent pixel image. Click tracking rewrites links to go through Lettr’s tracking domain. Both features respect user privacy and comply with email regulations.
Attach custom metadata for tracking and filtering:
response = client.emails.send(
from_email = "notifications@yourdomain.com" ,
to = [ "user@example.com" ],
subject = "Order confirmation" ,
html = "<p>Your order has been confirmed.</p>" ,
metadata = {
"user_id" : "12345" ,
"order_id" : "ORD-98765" ,
"campaign" : "abandoned-cart" ,
"environment" : "production" ,
},
)
Metadata is returned in webhook events and can be used to correlate emails with your application data.
Error Handling
The SDK provides structured exception types for different error scenarios:
from lettr.exceptions import (
LettrException,
LettrValidationException,
LettrAuthException,
LettrRateLimitException,
)
try :
response = client.emails.send(
from_email = "invalid@unverified-domain.com" ,
to = [ "user@example.com" ],
subject = "Test" ,
html = "<p>Test</p>" ,
)
print ( f "Email sent: { response.request_id } " )
except LettrValidationException as e:
# 422 validation errors
print ( f "Validation failed: { e.message } " )
for field, messages in e.errors.items():
print ( f " { field } : { messages } " )
except LettrAuthException as e:
# 401 authentication errors
print ( f "Authentication failed: { e.message } " )
except LettrRateLimitException as e:
# 429 rate limit errors
print ( f "Rate limit exceeded: { e.message } " )
except LettrException as e:
# Other API errors (500, 503, etc.)
print ( f "API error: { e.message } " )
except Exception as e:
# Network or other errors
print ( f "Request failed: { e } " )
Common Error Scenarios
Unverified domain:
Exception: LettrValidationException
Message: “The from address domain is not verified”
Solution: Verify your domain in the dashboard
Invalid API key:
Exception: LettrAuthException
Message: “Invalid API key”
Solution: Check your API key is correct and active
Rate limit exceeded:
Exception: LettrRateLimitException
Message: “Too many requests”
Solution: Implement exponential backoff retry logic
Async Support
The SDK provides full async support for non-blocking operations:
import asyncio
import lettr
async def send_emails ():
client = lettr.AsyncLettr(os.environ[ "LETTR_API_KEY" ])
# Send multiple emails concurrently
tasks = []
for recipient in [ "user1@example.com" , "user2@example.com" , "user3@example.com" ]:
task = client.emails.send(
from_email = "notifications@yourdomain.com" ,
to = [recipient],
subject = "Batch notification" ,
html = "<p>This is a batch email.</p>" ,
)
tasks.append(task)
responses = await asyncio.gather( * tasks, return_exceptions = True )
for i, response in enumerate (responses):
if isinstance (response, Exception ):
print ( f "Failed to send email { i + 1 } : { response } " )
else :
print ( f "Sent email { i + 1 } : { response.request_id } " )
asyncio.run(send_emails())
With Semaphore for Rate Limiting
Limit concurrent requests using asyncio.Semaphore:
import asyncio
import lettr
async def send_batch ( client , recipients , max_concurrent = 10 ):
semaphore = asyncio.Semaphore(max_concurrent)
async def send_with_limit ( recipient ):
async with semaphore:
return await client.emails.send(
from_email = "notifications@yourdomain.com" ,
to = [recipient],
subject = "Batch email" ,
html = "<p>Hello!</p>" ,
)
tasks = [send_with_limit(recipient) for recipient in recipients]
responses = await asyncio.gather( * tasks, return_exceptions = True )
for i, response in enumerate (responses):
if isinstance (response, Exception ):
print ( f "Failed: { response } " )
else :
print ( f "Sent to { recipients[i] } : { response.request_id } " )
async def main ():
client = lettr.AsyncLettr(os.environ[ "LETTR_API_KEY" ])
recipients = [ "user1@example.com" , "user2@example.com" , "user3@example.com" ]
await send_batch(client, recipients, max_concurrent = 5 )
asyncio.run(main())
For large batches, consider using a task queue like Celery or RQ to manage concurrent requests and handle failures gracefully.
Django Integration
Settings Configuration
Add Lettr configuration to your Django settings:
# settings.py
import os
LETTR_API_KEY = os.environ.get( "LETTR_API_KEY" )
Email Service
Create an email service module:
# services/email.py
from django.conf import settings
import lettr
client = lettr.Lettr(settings. LETTR_API_KEY )
def send_welcome_email ( recipient , name ):
"""Send a welcome email to a new user."""
response = client.emails.send(
from_email = "notifications@yourdomain.com" ,
to = [recipient],
subject = f "Welcome to Acme, { name } !" ,
html = f "<h1>Welcome, { name } !</h1><p>Thanks for signing up.</p>" ,
)
return response.request_id
Using in Views
Use the email service in your views:
# views.py
from django.http import JsonResponse
from .services.email import send_welcome_email
def register ( request ):
# ... registration logic ...
try :
request_id = send_welcome_email(user.email, user.name)
return JsonResponse({ "success" : True , "request_id" : request_id})
except Exception as e:
return JsonResponse({ "success" : False , "error" : str (e)}, status = 500 )
Best Practices
Use Environment Variables
Never hardcode API keys. Use environment variables or a secrets manager:
import os
api_key = os.environ.get( "LETTR_API_KEY" )
if not api_key:
raise ValueError ( "LETTR_API_KEY is required" )
client = lettr.Lettr(api_key)
Validate Before Sending
Validate email addresses before making API calls:
import re
def is_valid_email ( email ):
pattern = r ' ^ [ a-zA-Z0-9._%+- ] + @ [ a-zA-Z0-9.- ] + \. [ a-zA-Z ] {2,} $ '
return re.match(pattern, email) is not None
if not is_valid_email(recipient):
print ( f "Invalid email address: { recipient } " )
return
Log Request IDs
Always log the request_id from successful sends for tracking and debugging:
import logging
logger = logging.getLogger( __name__ )
response = client.emails.send( ... )
logger.info(
"Email sent successfully" ,
extra = {
"request_id" : response.request_id,
"accepted" : response.accepted,
"to" : to,
},
)
Handle Errors Gracefully
Implement retry logic for transient errors:
import time
from lettr.exceptions import LettrValidationException
def send_with_retry ( client , email_params , max_retries = 3 ):
for attempt in range (max_retries + 1 ):
try :
response = client.emails.send( ** email_params)
print ( f "Email sent: { response.request_id } " )
return response.request_id
except LettrValidationException:
# Don't retry validation errors
raise
except Exception as e:
if attempt < max_retries:
backoff = (attempt + 1 ) * 1.0
print ( f "Attempt { attempt + 1 } failed, retrying in { backoff } s: { e } " )
time.sleep(backoff)
else :
raise
raise Exception ( "Max retries exceeded" )
Use Context Managers
Use context managers to ensure proper cleanup:
import lettr
import httpx
with httpx.Client( timeout = 30.0 ) as http_client:
client = lettr.Lettr(api_key, http_client = http_client)
response = client.emails.send( ... )
Reuse the Client
Create a single client instance and reuse it across requests:
# Good: Create once, reuse
import lettr
client = lettr.Lettr(os.environ[ "LETTR_API_KEY" ])
def send_email ( to , subject , html ):
return client.emails.send(
from_email = "notifications@yourdomain.com" ,
to = [to],
subject = subject,
html = html,
)
# Bad: Creating new client for each request
def send_email ( to , subject , html ):
client = lettr.Lettr(os.environ[ "LETTR_API_KEY" ]) # Don't do this
return client.emails.send( ... )
Type Hints
The SDK includes full type hints for better IDE support:
from typing import List, Dict, Optional
import lettr
def send_notification (
client : lettr.Lettr,
recipients : List[ str ],
subject : str ,
html : str ,
metadata : Optional[Dict[ str , str ]] = None ,
) -> str :
"""Send a notification email and return the request ID."""
response = client.emails.send(
from_email = "notifications@yourdomain.com" ,
to = recipients,
subject = subject,
html = html,
metadata = metadata,
)
return response.request_id
Troubleshooting
If you see “The from address domain is not verified”:
Verify your domain in the Lettr dashboard
Ensure the from_email parameter uses the verified domain
Wait for DNS propagation (can take up to 48 hours)
See Domain Verification for help
Authentication failed (401 error)
If you see authentication errors:
Check your API key is correct and starts with lttr_
Verify the key is 68 characters total (prefix + 64 hex chars)
Ensure the key hasn’t been revoked in the dashboard
Confirm you’re reading from the correct environment variable
If requests timeout:
Increase the httpx client timeout
Check your network connectivity and firewall settings
Verify app.lettr.com is reachable
Use a custom httpx client with longer timeout settings
If you see “ModuleNotFoundError” or import errors:
Verify the package is installed: pip list | grep lettr
Reinstall the package: pip install --upgrade lettr
Check your Python version is 3.8 or later: python --version
Activate your virtual environment if using one
Rate limit exceeded (429 error)
If you’re hitting rate limits:
Implement exponential backoff retry logic (see Best Practices)
Use asyncio.Semaphore for controlled concurrent sending
Consider upgrading your Lettr plan for higher limits
Spread requests over time instead of bursts
If you encounter async-related errors:
Use AsyncLettr for async code, not Lettr
Ensure you’re using await with async methods
Run async functions with asyncio.run() at the top level
Check that your async runtime is properly configured
What’s Next