engineering
Transactional Email Best Practices: What Developers Get Wrong
The transactional email best practices I wish more developers followed: plain-text fallback, mobile rendering, auth, bounce webhooks, and timing.
Yogini Bende • 17 Jun, 2026
Akash Bhadange • 12 Nov, 2025 • engineering
When choosing how to send emails from your application, you'll encounter two primary approaches: SMTP (Simple Mail Transfer Protocol) and REST/HTTP APIs. This guide explains both methods, their tradeoffs, and helps you choose the right approach for your use case.
SMTP is the internet standard protocol for email transmission, established in 1982. When you send an email, whether through Gmail, Outlook, or a programmatic service, SMTP handles the actual delivery between mail servers.
How SMTP works:
An email API is a REST/HTTP interface that abstracts SMTP complexity. Instead of managing protocol-level commands, you make HTTP requests with JSON payloads to send emails.
How Email APIs work:
import smtplib
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
def send_email_smtp(to_email, subject, body):
smtp_server = "smtp.autosend.com"
smtp_port = 587
username = "your_username"
password = "your_password"
msg = MIMEMultipart()
msg['From'] = username
msg['To'] = to_email
msg['Subject'] = subject
msg.attach(MIMEText(body, 'plain'))
try:
server = smtplib.SMTP(smtp_server, smtp_port)
server.starttls()
server.login(username, password)
server.send_message(msg)
server.quit()
return True
except Exception as e:
print(f"Error: {e}")
return False
import requests
def send_email_api(to_email, subject, body):
api_url = "https://api.autosend.com/v1/send"
api_key = "your_api_key"
payload = {
"to": to_email,
"subject": subject,
"text": body,
"from": "[email protected]"
}
headers = {
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json"
}
try:
response = requests.post(api_url, json=payload, headers=headers)
response.raise_for_status()
return response.json()
except requests.exceptions.RequestException as e:
print(f"Error: {e}")
return None
SMTP: Requires managing persistent connections or creating new connections for each email. Connection pooling can improve performance but adds complexity.
API: Stateless HTTP requests. No connection management needed. HTTP clients handle connection pooling automatically.
SMTP: Uses username/password authentication. Some servers support OAuth2, but implementation varies.
API: Typically uses API keys or tokens in HTTP headers. More straightforward to implement and rotate credentials.
SMTP: Returns SMTP status codes (250 for success, 550 for rejection, etc.). Error messages can be cryptic and require parsing server responses.
550 5.1.1 <[email protected]>: Recipient address rejected
API: Returns structured JSON with clear error messages, error codes, and often includes request IDs for debugging.
{
"error": "invalid_recipient",
"message": "The email address is not valid",
"request_id": "req_abc123"
}
SMTP: Limited to basic email sending. Advanced features require custom headers or complex MIME construction.
API: Built-in support for:
SMTP: Often enforced but not transparent. You discover limits when connections are rejected or throttled.
API: Clearly documented rate limits. HTTP headers often include remaining quota (X-RateLimit-Remaining).
SMTP: Limited visibility. Must parse logs and SMTP responses. Difficult to trace individual emails.
API: Request IDs, detailed logs, dashboard analytics, and webhook events provide comprehensive visibility.
SMTP:
With connection reuse, subsequent emails: 50-300ms each.
API:
APIs are often faster for single emails due to optimized infrastructure.
SMTP: Can send multiple emails over a single connection (pipelining). Ideal for high-volume batch sending when implemented correctly.
API: Limited by HTTP request overhead, but easier to parallelize. Most services support batch endpoints for sending multiple emails in one request.
SMTP: Lower bandwidth for multiple emails on same connection. More CPU for managing connections.
API: Slightly higher bandwidth due to HTTP overhead. Lower CPU due to simpler implementation.
SMTP:
API:
SMTP: Username/password transmitted during authentication. If connection isn't encrypted, credentials can be intercepted.
API: API keys in HTTPS headers. Less vulnerable to credential stuffing attacks. Easier to implement key rotation.
SMTP: Typically all-or-nothing access to send emails.
API: Granular permissions. Different API keys for different environments or applications. Can restrict by IP, domain, or feature.
Many email services offer both SMTP and API access. Whichever you pick, the transactional email best practices are the same. Consider using both:
Benefits:
Challenges:
Benefits:
Challenges:
Both SMTP and APIs ultimately deliver via SMTP to recipient servers. Deliverability depends on:
The sending method (SMTP vs API) doesn't directly impact deliverability, but APIs often provide better tools for monitoring and improving it.
SMTP:
from email.mime.base import MIMEBase
from email import encoders
msg = MIMEMultipart()
# ... set headers ...
with open("document.pdf", "rb") as f:
part = MIMEBase("application", "pdf")
part.set_payload(f.read())
encoders.encode_base64(part)
part.add_header("Content-Disposition", "attachment; filename=document.pdf")
msg.attach(part)
API:
import base64
with open("document.pdf", "rb") as f:
encoded = base64.b64encode(f.read()).decode()
payload = {
"to": "[email protected]",
"subject": "Document attached",
"text": "Please see attached",
"attachments": [{
"filename": "document.pdf",
"content": encoded,
"type": "application/pdf"
}]
}
SMTP:
from email.mime.text import MIMEText
html = """
<html>
<body>
<h1>Welcome!</h1>
<p>Thanks for signing up.</p>
</body>
</html>
"""
msg = MIMEMultipart('alternative')
msg.attach(MIMEText("Welcome! Thanks for signing up.", 'plain'))
msg.attach(MIMEText(html, 'html'))
API:
payload = {
"to": "[email protected]",
"subject": "Welcome",
"text": "Welcome! Thanks for signing up.",
"html": "<h1>Welcome!</h1><p>Thanks for signing up.</p>"
}
API services typically cost 20-50% more but include additional features that might require separate tools with SMTP-only services.
import logging
logging.basicConfig(level=logging.DEBUG)
logger = logging.getLogger('smtp')
server = smtplib.SMTP(smtp_server, smtp_port)
server.set_debuglevel(1) # Enable debug output
Debug output shows protocol-level communication but requires parsing for structured logging.
response = requests.post(api_url, json=payload, headers=headers)
log_data = {
"email_id": response.json().get("id"),
"status": response.status_code,
"recipient": payload["to"],
"timestamp": datetime.now().isoformat()
}
logger.info("Email sent", extra=log_data)
Structured JSON responses integrate naturally with logging systems.
import time
def send_with_retry(to_email, subject, body, max_retries=3):
for attempt in range(max_retries):
try:
return send_email_smtp(to_email, subject, body)
except smtplib.SMTPException as e:
if attempt == max_retries - 1:
raise
wait_time = 2 ** attempt # Exponential backoff
time.sleep(wait_time)
Most HTTP libraries have built-in retry mechanisms:
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
session = requests.Session()
retry = Retry(
total=3,
backoff_factor=1,
status_forcelist=[429, 500, 502, 503, 504]
)
adapter = HTTPAdapter(max_retries=retry)
session.mount('https://', adapter)
import smtplib
from unittest.mock import Mock, patch
def test_email_sending():
with patch('smtplib.SMTP') as mock_smtp:
mock_server = Mock()
mock_smtp.return_value = mock_server
send_email_smtp("[email protected]", "Test", "Body")
mock_server.starttls.assert_called_once()
mock_server.login.assert_called_once()
mock_server.send_message.assert_called_once()
import responses
@responses.activate
def test_email_api():
responses.add(
responses.POST,
'https://api.emailservice.com/v1/send',
json={"id": "msg_123", "status": "queued"},
status=200
)
result = send_email_api("[email protected]", "Test", "Body")
assert result["id"] == "msg_123"
API testing is generally simpler with HTTP mocking libraries.
Ask yourself these questions:
What's your email volume?
What features do you need?
What's your team's expertise?
What's your infrastructure?
How important is developer velocity?
Neither SMTP nor APIs are universally better. The right choice depends on your specific requirements:
Most modern applications benefit from starting with an API for ease of use, then potentially adding SMTP for high-volume use cases as they scale.
The email service provider you choose matters more than the protocol. Look for providers offering both options, strong deliverability, good documentation, and responsive support.
Related Articles
Still wondering?
See what your favorite LLM has to say about us,
then make an informed decision.