18.5. Transactional Emails
Table of Contents
Why Transactional Emails Matter
Transactional emails are emails that are triggered by what a user or system does, not by marketing campaigns. They are part of your backend’s core functionality, just like saving to a database or processing a payment.
Typical examples:
- Password reset emails
- Email verification / account activation
- Order confirmation and receipts
- Shipping updates
- Invoice and payment notifications
- Security alerts, such as login from a new device
If these emails fail, users cannot complete important actions. So as a backend developer you must treat transactional emails as critical infrastructure, not a nice-to-have extra.
Transactional emails are functional emails triggered by a specific event, and users generally expect them. Unlike marketing emails, they should be sent only in direct response to actions or system events.
They often:
- Must be delivered quickly (seconds or a few minutes)
- Must be reliable and traceable
- Contain sensitive or personal data
- Have legal or compliance implications, for example invoices
Because of this, you should design, implement, and monitor them carefully.
Transactional vs Marketing Emails
It is important to clearly separate these two categories in your codebase and configuration.
| Aspect | Transactional Email | Marketing / Newsletter Email |
|---|---|---|
| Trigger | User or system event | Business / campaign decision |
| Purpose | Complete a process or provide important info | Promote, sell, or engage |
| User expectation | Expected and often required | Optional, user may opt in or out |
| Examples | Password reset, receipt, alerts | Promotions, product news, weekly digest |
| Legal treatment | Often allowed even if user unsubscribes from marketing | Usually requires explicit consent (opt-in) |
| Frequency | Occurs only when event happens | Broadcast to many users at once |
Some rules of thumb:
- Do not mix marketing content into transactional emails.
- Do not send bulk campaigns with your transactional infrastructure.
- Use separate API keys, IPs, or subdomains when possible, for example:
- transactional.example.com
- newsletter.example.com
This separation protects your deliverability. If marketing campaigns get marked as spam, your crucial password reset emails should not start going to spam too.
Common Types of Transactional Emails
Focus on the ones backend services send most often.
Account-related emails
These are required for basic account flows.
- Welcome email
Sent after sign-up. Confirms the account creation and often includes: - Short welcome message
- Link to the app
- Sometimes an email verification link
- Email verification / account activation
Confirms that the user owns the provided email address. Usually includes: - Verification link with a time-limited token
- Clear message: “Verify your email to activate your account.”
- Password reset email
Allows users to change a forgotten password. Usually includes: - One-time, short-lived token in a URL
- Expiration information
- Possibly the IP or location of the reset request
Security and access emails
These help keep accounts secure.
- Login from new device / new location
Notifies users about suspicious access. Can include: - Device or browser information
- IP address and approximate location
- Time of login
- Link to secure the account if it was not them
- Two-factor authentication (2FA) codes
If you use email as a second factor, send a short code, for example 6 digits. Must be: - Short-lived
- One-time use
- Email change confirmation
When a user changes their email, you might: - Send a notification to the old address
- Send a confirmation link to the new address
Order and billing emails
Very common in e-commerce and SaaS backends.
- Order confirmation
Sent after a successful checkout. Typically includes: - Order ID
- Items, quantities, prices
- Billing and shipping addresses
- Estimated delivery date
- Shipping / delivery updates
Sent when order status changes: - Shipped
- Out for delivery
- Delivered
- Invoices and receipts
Important for accounting. Often attached as PDFs or linked. Includes: - Invoice number
- Tax information
- Payment method and status
- Payment failure / subscription issues
Sent when a charge fails or a subscription needs attention. Should: - Explain the problem
- Provide a clear action, like “Update your card.”
Activity notifications
These depend on your product but follow similar patterns.
- Comments or replies to posts
- Mentions or tags in social apps
- Project updates in collaboration tools
- System alerts, for example “Your export is ready, download it here.”
In all cases, an event in your system triggers the email.
Designing Transactional Email Flows
Treat transactional emails as part of your backend’s core workflow. For each important user flow, design:
- Trigger
What exactly causes the email? For example: - “User submits password reset form.”
- “Payment provider sends webhook that a payment succeeded.”
- Data needed
Gather all data needed to build the email without additional queries if possible: - Recipient email
- User name / display name
- Order data, token, or other specific information
- Template selection
Decide which template to use, for example: password_reset.htmlandpassword_reset.txtorder_confirmation.html
Many email providers support templates stored on their side and referenced by an ID.
- Token or link generation if needed
For flows like password reset or verification, generate secure links of the form:
https://app.example.com/reset-password?token=...https://app.example.com/verify-email?token=...
The backend must validate and store these tokens securely. Details of token creation belong in the authentication chapters, so we only note here that the email contains the token and the backend validates it.
- Send strategy
Decide how you will send:
- Synchronously in the HTTP request handler
- Or by queuing a background job and responding to the client immediately
For most real systems, using background jobs is better. We discuss this more below.
- Error handling
What if sending fails? Some options:
- Retry automatically in the background
- Notify admins or log an error
- Expose a safe message to the user, for example “If you do not receive an email, please try again.”
Design each email type as a small flow with clear triggers, steps, and outcomes.
Implementing Transactional Emails in Your Backend
Most modern backends use a third-party email provider instead of running their own SMTP server. The typical flow looks like this:
- Your backend connects to the provider via HTTP API
- You call an “send email” endpoint with JSON data
- The provider takes care of actual delivery, queuing, and retries
Example: Basic password reset email flow
To show the moving parts, here is a simple Python-style pseudo flow:
- User submits their email address to
/password-reset/request - Backend:
- Finds the user by email
- Generates a secure token (for example a random string, stored with expiration)
- Builds a reset URL, for example
https://app.example.com/reset?token=XYZ - Enqueues a background job
send_password_reset_email(user_email, reset_url) - Worker process picks up the job and calls the email provider API:
def send_password_reset_email(user_email: str, reset_url: str) -> None:
subject = "Reset your password"
html_body = f"""
<p>Hello,</p>
<p>We received a request to reset your password.</p>
<p><a href="{reset_url}">Click here to reset your password</a></p>
<p>This link will expire in 30 minutes.</p>
"""
text_body = (
"Hello,\n\n"
"We received a request to reset your password.\n"
f"Open this link to reset it: {reset_url}\n\n"
"This link will expire in 30 minutes.\n"
)
email_api.send(
to=user_email,
subject=subject,
html=html_body,
text=text_body,
)- When the user clicks the link, the frontend calls an API endpoint, for example
/password-reset/confirm, with the token and new password. - Backend validates the token and updates the password.
The key idea for this chapter is that your backend uses transactional emails as part of stateful flows and therefore must handle:
- Token and link creation
- Secure storage
- Timing and expiration
- Sending and errors
Templates and Personalization
Transactional emails are very repetitive in structure but personalized to each user or event.
Template structure
A simple email template might look like:
<!DOCTYPE html>
<html>
<body>
<p>Hello {{ user_name }},</p>
<p>Thank you for your order <strong>#{{ order_id }}</strong>.</p>
<p>Order summary:</p>
<ul>
{% for item in items %}
<li>{{ item.name }} (x{{ item.quantity }}) - {{ item.price }}</li>
{% endfor %}
</ul>
<p>Total: {{ total }}</p>
</body>
</html>In your backend code you then pass data like:
context = {
"user_name": "Alice",
"order_id": "12345",
"items": [
{"name": "Book", "quantity": 1, "price": "$10"},
{"name": "Pen", "quantity": 2, "price": "$2"},
],
"total": "$14",
}Your templating engine or email provider fills in the placeholders.
Personalization options
You can safely personalize:
- User name or display name
- Order contents
- Payment date, last 4 digits of card
- Account data relevant to the event
Avoid including:
- Full credit card numbers
- Sensitive passwords or secrets
- Very long or complex HTML with heavy images, which may hurt deliverability
Keep transactional emails mostly functional and minimal, with clear calls to action.
Sending Emails Reliably and at Scale
In small demos, you might send emails directly inside your HTTP handlers, for example:
def register_user():
# save user to database
send_welcome_email(user_email)
return {"message": "Registered"}For real-world systems this approach has problems:
- Increases request latency
- If the email provider is slow or down, your endpoint fails
- Retries are hard to manage
A better pattern is to treat email sending as a background job.
Synchronous vs asynchronous sending
| Approach | Description | Pros | Cons |
|---|---|---|---|
| Synchronous | Send email inside the HTTP request handler | Simple to implement | Slower responses, less reliable |
| Asynchronous (queue) | Enqueue job and let a worker send in the background | Fast responses, robust retries | Requires job queue / worker infrastructure |
In other chapters we cover tools like Celery and message queues. For transactional emails, it is common to:
- Enqueue a job with type and payload, for example:
- job type:
send_transactional_email - payload:
{ "template": "password_reset", "to": "...", "data": {...} } - A worker process reads jobs and calls the email provider.
Idempotency concerns
You should think about what happens if the same email job runs twice:
- For some emails, sending twice is acceptable, for example order confirmation
- For others, it might be confusing but not dangerous, for example password reset
To reduce duplicate sends, you can store an email_events table with:
user_idevent_type, such asPASSWORD_RESET_REQUESTcreated_at
When sending, you can check if a similar event happened very recently and decide whether to send again. How strict you are depends on the business rules.
Deliverability and Best Practices
Good transactional email design is not only about code. You must also ensure that emails actually reach inboxes.
Technical configuration (high level)
Your DevOps or operations setup should include:
- SPF records
- DKIM signatures
- DMARC policies
These are DNS and cryptographic settings that tell email providers your emails are legitimate. The exact configuration is usually guided by your email provider.
As a backend developer you often:
- Provide application domains and subdomains
- Generate keys if needed
- Test sending from test accounts
Content best practices
For transactional emails:
- Use clear subjects, for example:
- “Reset your password”
- “Your order #12345 has been shipped”
- Keep the content simple and to the point
- Include a clear call to action, for example button or link
- Provide some minimal context, such as:
- When the action was requested
- If they did not initiate it, how to secure their account
Remember that:
Never include plain-text passwords or long-lived secrets in transactional emails. Always send links with time-limited tokens, not the actual secret.
Monitoring and logging
You should log:
- When you attempt to send a transactional email
- Whether the provider accepted it or reported an error
- The type of email and the recipient (or anonymized identifier)
Log examples:
INFO [email] send password_reset to user_id=42 status=accepted
ERROR [email] send order_confirmation to user_id=7 error="provider timeout"In larger systems you may also:
- Store provider webhook events about deliveries, opens, and bounces
- Build an internal dashboard of email status per user
This helps debug user complaints like “I did not receive my email.”
Simple Example Flows
To connect everything, here are two compact flows you will likely implement in your projects.
Flow 1: Email verification after registration
- User registers with email and password.
- Backend:
- Creates user in database with
is_email_verified = False - Generates an email verification token with expiration
- Enqueues
send_email_verification(user_email, verification_link) - Worker sends an email:
- Subject: “Verify your email address”
- Body: Link like
https://app.example.com/verify?token=... - User clicks the link.
- Backend validates token, marks
is_email_verified = True, and maybe redirects to a welcome page.
Flow 2: Order confirmation
- User checks out and pays.
- Payment provider calls your webhook with “payment succeeded.”
- Backend:
- Creates an order record
- Updates inventory
- Enqueues
send_order_confirmation(user_email, order_id) - Worker loads order data from database and sends an email with:
- Order items
- Prices and totals
- Billing and shipping information
Each flow:
- Uses clear triggers
- Builds the correct data
- Uses a template
- Sends via a provider, often in the background
This is the core pattern behind nearly all transactional emails in backend systems.
Views: 7
KAHIBARO