Sending an email via Senderr using Python
This guide shows you how to integrate Senderr email templates into your Python applications using the requests library and Resend for email delivery.Prerequisites
Before you begin, make sure you have:- Python 3.7 or higher
- A Senderr account with templates in your library
- A Senderr API key (generate one here)
- A Resend account for sending emails (sign up here)
Installation
Install the required packages:Copy
pip install requests resend
requirements.txt:
Copy
requests>=2.31.0
resend>=0.6.0
Basic Setup
Environment Configuration
Create a.env file or set environment variables:
Copy
export SENDERR_API_KEY="your_senderr_api_key_here"
export RESEND_API_KEY="your_resend_api_key_here"
Basic Senderr Client
Create a simple client class for interacting with Senderr:Copy
import os
import requests
from typing import Dict, List, Optional, Any
class SenderrClient:
def __init__(self, api_key: str = None):
self.api_key = api_key or os.getenv('SENDERR_API_KEY')
self.base_url = 'https://senderr.dev/api/v1'
self.session = requests.Session()
self.session.headers.update({
'Authorization': f'Bearer {self.api_key}',
'Content-Type': 'application/json'
})
def get_library(self) -> List[Dict]:
"""Get all templates in your library."""
response = self.session.get(f'{self.base_url}/library')
response.raise_for_status()
return response.json()['templates']
def get_template_schema(self, template_id: str) -> Dict:
"""Get the variable schema for a template."""
response = self.session.get(f'{self.base_url}/templates/{template_id}/schema')
response.raise_for_status()
return response.json()
def render_template(self, template_id: str, variables: Dict, format: str = 'html') -> Dict:
"""Render a template with the provided variables."""
payload = {
'variables': variables,
'format': format
}
response = self.session.post(
f'{self.base_url}/templates/{template_id}/render',
json=payload
)
response.raise_for_status()
return response.json()
Email Service Integration
Resend Integration
Copy
import resend
from typing import Dict, List, Optional
class EmailService:
def __init__(self, api_key: str = None):
self.api_key = api_key or os.getenv('RESEND_API_KEY')
resend.api_key = self.api_key
def send_email(
self,
to: str | List[str],
subject: str,
html_content: str,
from_email: str = "[email protected]",
reply_to: Optional[str] = None
) -> Dict:
"""Send an email using Resend."""
params = {
"from": from_email,
"to": to if isinstance(to, list) else [to],
"subject": subject,
"html": html_content,
}
if reply_to:
params["reply_to"] = reply_to
try:
email = resend.Emails.send(params)
return {"success": True, "id": email.get("id")}
except Exception as e:
return {"success": False, "error": str(e)}
Complete Example: Welcome Email
Here’s a complete example that sends a welcome email using a Senderr template:Copy
import os
from datetime import datetime
from dataclasses import dataclass
from typing import Optional
@dataclass
class User:
email: str
first_name: str
last_name: str
company: Optional[str] = None
signup_date: Optional[datetime] = None
class WelcomeEmailService:
def __init__(self):
self.senderr = SenderrClient()
self.email_service = EmailService()
self.welcome_template_id = "your_welcome_template_id" # Set your template ID
def send_welcome_email(self, user: User) -> Dict:
"""Send a welcome email to a new user."""
try:
# Prepare template variables
variables = {
"firstName": user.first_name,
"lastName": user.last_name,
"email": user.email,
"companyName": user.company or "there",
"signupDate": user.signup_date.strftime("%B %d, %Y") if user.signup_date else datetime.now().strftime("%B %d, %Y"),
"dashboardUrl": "https://yourapp.com/dashboard",
"supportEmail": "[email protected]"
}
# Render the template
rendered = self.senderr.render_template(
self.welcome_template_id,
variables
)
# Send the email
result = self.email_service.send_email(
to=user.email,
subject=f"Welcome to our platform, {user.first_name}!",
html_content=rendered['html']
)
return result
except requests.exceptions.RequestException as e:
return {"success": False, "error": f"Template rendering failed: {str(e)}"}
except Exception as e:
return {"success": False, "error": f"Email sending failed: {str(e)}"}
# Usage
if __name__ == "__main__":
# Create a new user
new_user = User(
email="[email protected]",
first_name="John",
last_name="Doe",
company="Acme Corp",
signup_date=datetime.now()
)
# Send welcome email
welcome_service = WelcomeEmailService()
result = welcome_service.send_welcome_email(new_user)
if result['success']:
print(f"Welcome email sent successfully! ID: {result['id']}")
else:
print(f"Failed to send email: {result['error']}")
Advanced Features
Template Management
Copy
class TemplateManager:
def __init__(self, senderr_client: SenderrClient):
self.client = senderr_client
self._template_cache = {}
def find_template_by_name(self, name: str) -> Optional[Dict]:
"""Find a template by its name."""
templates = self.client.get_library()
for template in templates:
if template['name'].lower() == name.lower():
return template
return None
def get_cached_schema(self, template_id: str) -> Dict:
"""Get template schema with caching."""
if template_id not in self._template_cache:
self._template_cache[template_id] = self.client.get_template_schema(template_id)
return self._template_cache[template_id]
def validate_variables(self, template_id: str, variables: Dict) -> List[str]:
"""Validate that all required variables are provided."""
schema = self.get_cached_schema(template_id)
required_vars = schema.get('required', [])
missing_vars = []
for var in required_vars:
if var not in variables or variables[var] is None:
missing_vars.append(var)
return missing_vars
Bulk Email Sending
Copy
import asyncio
import aiohttp
from concurrent.futures import ThreadPoolExecutor
from typing import List
class BulkEmailService:
def __init__(self, max_workers: int = 5):
self.senderr = SenderrClient()
self.email_service = EmailService()
self.max_workers = max_workers
def send_bulk_emails(
self,
template_id: str,
recipients: List[Dict],
base_variables: Dict = None
) -> List[Dict]:
"""Send emails to multiple recipients with personalized variables."""
def send_single_email(recipient):
try:
# Merge base variables with recipient-specific variables
variables = {**(base_variables or {}), **recipient['variables']}
# Render template
rendered = self.senderr.render_template(template_id, variables)
# Send email
result = self.email_service.send_email(
to=recipient['email'],
subject=recipient.get('subject', 'Newsletter'),
html_content=rendered['html']
)
return {
"email": recipient['email'],
"success": result['success'],
"id": result.get('id'),
"error": result.get('error')
}
except Exception as e:
return {
"email": recipient['email'],
"success": False,
"error": str(e)
}
# Use ThreadPoolExecutor for concurrent sending
with ThreadPoolExecutor(max_workers=self.max_workers) as executor:
results = list(executor.map(send_single_email, recipients))
return results
# Usage example
bulk_service = BulkEmailService()
recipients = [
{
"email": "[email protected]",
"variables": {"firstName": "John", "lastName": "Doe"},
"subject": "Monthly Newsletter"
},
{
"email": "[email protected]",
"variables": {"firstName": "Jane", "lastName": "Smith"},
"subject": "Monthly Newsletter"
}
]
results = bulk_service.send_bulk_emails("newsletter_template_id", recipients)
print(f"Sent {sum(1 for r in results if r['success'])} out of {len(results)} emails")
Error Handling and Retry Logic
Copy
import time
from functools import wraps
def retry_on_failure(max_retries: int = 3, delay: float = 1.0):
"""Decorator to retry failed operations."""
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
for attempt in range(max_retries):
try:
return func(*args, **kwargs)
except requests.exceptions.RequestException as e:
if attempt == max_retries - 1:
raise e
time.sleep(delay * (2 ** attempt)) # Exponential backoff
return None
return wrapper
return decorator
class RobustSenderrClient(SenderrClient):
@retry_on_failure(max_retries=3)
def render_template(self, template_id: str, variables: Dict, format: str = 'html') -> Dict:
"""Render template with retry logic."""
return super().render_template(template_id, variables, format)
Django Integration Example
Copy
# models.py
from django.db import models
class EmailTemplate(models.Model):
name = models.CharField(max_length=200)
senderr_template_id = models.CharField(max_length=100)
description = models.TextField()
is_active = models.BooleanField(default=True)
class EmailLog(models.Model):
template = models.ForeignKey(EmailTemplate, on_delete=models.CASCADE)
recipient_email = models.EmailField()
subject = models.CharField(max_length=200)
sent_at = models.DateTimeField(auto_now_add=True)
success = models.BooleanField()
error_message = models.TextField(blank=True)
# services.py
from django.conf import settings
from .models import EmailTemplate, EmailLog
class DjangoEmailService:
def __init__(self):
self.senderr = SenderrClient()
self.email_service = EmailService()
def send_template_email(
self,
template_name: str,
recipient_email: str,
variables: Dict,
subject: str
) -> bool:
"""Send email using a Django model template."""
try:
# Get template from database
template = EmailTemplate.objects.get(name=template_name, is_active=True)
# Render template
rendered = self.senderr.render_template(
template.senderr_template_id,
variables
)
# Send email
result = self.email_service.send_email(
to=recipient_email,
subject=subject,
html_content=rendered['html']
)
# Log the result
EmailLog.objects.create(
template=template,
recipient_email=recipient_email,
subject=subject,
success=result['success'],
error_message=result.get('error', '')
)
return result['success']
except EmailTemplate.DoesNotExist:
EmailLog.objects.create(
template=None,
recipient_email=recipient_email,
subject=subject,
success=False,
error_message=f"Template '{template_name}' not found"
)
return False
except Exception as e:
EmailLog.objects.create(
template=template if 'template' in locals() else None,
recipient_email=recipient_email,
subject=subject,
success=False,
error_message=str(e)
)
return False
Flask Integration Example
Copy
from flask import Flask, request, jsonify
from flask_sqlalchemy import SQLAlchemy
app = Flask(__name__)
db = SQLAlchemy(app)
class EmailQueue(db.Model):
id = db.Column(db.Integer, primary_key=True)
recipient_email = db.Column(db.String(255), nullable=False)
template_id = db.Column(db.String(100), nullable=False)
variables = db.Column(db.JSON)
status = db.Column(db.String(50), default='pending')
created_at = db.Column(db.DateTime, default=datetime.utcnow)
@app.route('/api/send-email', methods=['POST'])
def send_email():
data = request.get_json()
# Validate input
required_fields = ['email', 'template_id', 'variables']
if not all(field in data for field in required_fields):
return jsonify({'error': 'Missing required fields'}), 400
try:
# Initialize services
senderr = SenderrClient()
email_service = EmailService()
# Render template
rendered = senderr.render_template(
data['template_id'],
data['variables']
)
# Send email
result = email_service.send_email(
to=data['email'],
subject=data.get('subject', 'Email from Senderr'),
html_content=rendered['html']
)
if result['success']:
return jsonify({'success': True, 'message': 'Email sent successfully'})
else:
return jsonify({'success': False, 'error': result['error']}), 500
except Exception as e:
return jsonify({'success': False, 'error': str(e)}), 500
if __name__ == '__main__':
app.run(debug=True)
Testing
Copy
import unittest
from unittest.mock import Mock, patch
class TestSenderrClient(unittest.TestCase):
def setUp(self):
self.client = SenderrClient(api_key="test_key")
@patch('requests.Session.get')
def test_get_library(self, mock_get):
# Mock successful response
mock_response = Mock()
mock_response.json.return_value = {'templates': [{'id': '123', 'name': 'Test'}]}
mock_response.raise_for_status.return_value = None
mock_get.return_value = mock_response
# Test
templates = self.client.get_library()
# Assert
self.assertEqual(len(templates), 1)
self.assertEqual(templates[0]['name'], 'Test')
@patch('requests.Session.post')
def test_render_template(self, mock_post):
# Mock successful response
mock_response = Mock()
mock_response.json.return_value = {'html': '<h1>Hello World</h1>'}
mock_response.raise_for_status.return_value = None
mock_post.return_value = mock_response
# Test
result = self.client.render_template('123', {'name': 'John'})
# Assert
self.assertIn('html', result)
self.assertEqual(result['html'], '<h1>Hello World</h1>')
if __name__ == '__main__':
unittest.main()
Best Practices
1. Error Handling
Always handle API errors gracefully:- Network timeouts
- Invalid API keys
- Template not found
- Missing variables
2. Caching
Cache template schemas and frequently used templates to reduce API calls.3. Rate Limiting
Respect API rate limits by implementing backoff strategies.4. Security
- Store API keys securely (environment variables, secrets management)
- Validate user input before passing to templates
- Use HTTPS for all API calls
5. Monitoring
Log email sending results for debugging and monitoring purposes.Next Steps
- Check out n8n integration
- Learn about TypeScript integration
- Explore the API reference
- Browse available templates