Skip to main content

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:
pip install requests resend
Or add to your requirements.txt:
requests>=2.31.0
resend>=0.6.0

Basic Setup

Environment Configuration

Create a .env file or set environment variables:
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:
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

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:
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

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

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

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

# 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

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

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

Need help? Contact [email protected]