skills/sending/transactional-email/SKILL.md
Design and send transactional emails. Use when building password resets, receipts, shipping notifications, account alerts, or separating transactional from marketing streams.
npx skillsauth add chunkydotdev/email-skills transactional-emailInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Send receipts, auth codes, shipping notifications, and account alerts that arrive reliably and stay compliant.
domain-authentication - SPF, DKIM, DMARC setup required before sending anythingprovider-setup - choosing and configuring an email providerbounce-handling - processing hard/soft bounces and retry strategiesemail-compliance - CAN-SPAM, GDPR, CASL, and unsubscribe requirementstemplate-design - HTML email templates that render everywherenotification-design - product notification email patternsThe FTC's CAN-SPAM Act defines transactional email by its "primary purpose." A message is transactional if it:
| Category | Examples | Time sensitivity | |----------|----------|-----------------| | Authentication | Password resets, email verification, 2FA codes | Seconds | | Transaction confirmations | Order receipts, payment confirmations, refund notices | Minutes | | Shipping & delivery | Tracking updates, delivery confirmations, return labels | Minutes to hours | | Account alerts | Password changed, new device login, billing failed | Seconds to minutes | | Subscription lifecycle | Trial expiring, plan upgraded, payment method expiring | Hours |
These are commercial messages, even if they feel operational:
The FTC uses a "reasonable person" standard: if a recipient reading the subject line would conclude the message is an advertisement, it's commercial. The FTC settled with Experian over emails labeled "this is not a marketing email" that were actually pitching new products.
This is where most teams get in trouble. CAN-SPAM allows mixed content, but the "primary purpose" test determines which rules apply.
An email with both transactional and commercial content is judged commercial if:
To keep a mixed email classified as transactional:
Transactional emails are exempt from most CAN-SPAM requirements (unsubscribe links, physical address, opt-out honoring). But if your "transactional" email is actually commercial, you're violating CAN-SPAM at up to $51,744 per email (2025 adjusted amount).
More practically: if you send commercial content through your transactional email stream, mailbox providers notice. Your transactional IP/domain reputation drops, and your actual transactional emails start landing in spam. Password resets in the spam folder is a support nightmare.
The single most important architectural decision for transactional email is keeping it on separate infrastructure from marketing email.
Marketing emails have volatile reputation. A promotional blast that triggers spam complaints drags down every email on that IP or subdomain - including your password resets. Separation protects transactional delivery from marketing reputation fluctuations.
| Layer | Transactional | Marketing |
|-------|--------------|-----------|
| Subdomain | mail.example.com | news.example.com |
| SPF record | Separate per subdomain | Separate per subdomain |
| DKIM selector | Dedicated selector | Different selector |
| IP address | Dedicated IP (if volume supports it) | Separate IP pool |
| Provider account | Separate account or subaccount | Separate account or subaccount |
| Return-Path | [email protected] | [email protected] |
Choose a provider optimized for transactional delivery:
Providers like Postmark that refuse marketing email maintain better IP reputation specifically because they keep the streams separate at the infrastructure level.
Services like molted.email enforce stream separation through policy rules, automatically classifying sends as transactional or commercial and routing them through appropriate infrastructure.
If you can't do full separation immediately:
txn.example.com vs mktg.example.com)Transactional emails have delivery expectations that marketing emails don't. A password reset that arrives 5 minutes late is a failed password reset.
| Email type | Target | Unacceptable | |------------|--------|-------------| | 2FA codes, password resets | Under 10 seconds | Over 30 seconds | | Email verification | Under 30 seconds | Over 2 minutes | | Order confirmations | Under 1 minute | Over 5 minutes | | Shipping notifications | Under 5 minutes | Over 30 minutes | | Account alerts | Under 1 minute | Over 5 minutes |
Track the time between the triggering event (user clicked "reset password") and provider acceptance (the ESP returns a message ID). Alert if this exceeds your target threshold. Provider delivery to the inbox is harder to measure, but most transactional ESPs report it in their dashboards.
Automated systems send duplicate emails more often than humans realize. A retry after a timeout, a race condition between microservices, or a queued job that runs twice - any of these sends the same receipt or auth code twice.
Every transactional send should include a deduplication key - a unique identifier for the logical send operation, not the API call.
dedupe key = <event-type>:<entity-id>:<event-id>
Examples:
password-reset:user-123:reset-req-456
order-confirmation:order-789:v1
shipping-update:shipment-012:status-delivered
If the system receives a second send request with the same dedupe key, it returns the result of the first send instead of sending again.
# First call - sends the email
curl -X POST https://api.your-esp.com/v1/send \
-H 'Content-Type: application/json' \
-d '{
"to": "[email protected]",
"template": "order-confirmation",
"dedupeKey": "order-confirm:ord_abc123:v1",
"payload": { "orderId": "ord_abc123", "total": "$49.99" }
}'
# Second call (retry after timeout) - returns cached result, no duplicate send
curl -X POST https://api.your-esp.com/v1/send \
-H 'Content-Type: application/json' \
-d '{
"to": "[email protected]",
"template": "order-confirmation",
"dedupeKey": "order-confirm:ord_abc123:v1",
"payload": { "orderId": "ord_abc123", "total": "$49.99" }
}'
Even without ESP-level deduplication, you can prevent duplicates at your application layer:
-- Create send record with unique constraint
INSERT INTO transactional_sends (dedupe_key, recipient, template, status)
VALUES ('password-reset:user-123:req-456', '[email protected]', 'password-reset', 'pending')
ON CONFLICT (dedupe_key) DO NOTHING
RETURNING id;
-- If no row returned, a send with this key already exists - skip it
Dedupe keys should expire after a reasonable window. A password reset dedupe key from 24 hours ago shouldn't block a new password reset today. Common TTLs:
Subject line: Be direct. "Reset your password" or "Your verification code: 847291". Never use clickbait or branding-heavy subjects on auth emails.
Body structure:
Key rules:
Subject line: "Your order #12345 is confirmed" or "Receipt for your purchase"
Body structure:
Key rules:
Subject line: "Your order #12345 has shipped" with carrier and tracking number
Body structure:
Key rules:
Subject line: "Your password was changed" or "New sign-in from Chrome on Windows"
Body structure:
Key rules:
Transactional emails have fundamentally different engagement patterns than marketing emails because recipients are expecting them and need them.
| Metric | Transactional email | Marketing email | |--------|-------------------|-----------------| | Open rate | 60-90% | 15-25% | | Click-through rate | 10-30% | 1-5% | | Bounce rate target | Under 1% | Under 2% | | Complaint rate target | Under 0.01% | Under 0.1% | | Expected delivery speed | Seconds to minutes | Minutes to hours |
If your transactional emails have open rates below 40%, something is wrong - either they're landing in spam, your list has stale addresses, or you're misclassifying commercial emails as transactional.
A user who requested a password reset and received one has no reason to mark it as spam. If your transactional complaint rate exceeds 0.01%, investigate:
Transactional emails should be functional, not beautiful. Heavy HTML templates cause problems:
A clean, mostly-text template with your logo, clear typography, and one primary call-to-action button outperforms elaborate designs for transactional email.
When you update a transactional template, you need to handle in-flight sends:
Services like molted.email provide built-in template versioning with approval workflows and lint checks, ensuring transactional templates pass content validation before going live.
Every transactional template should expose typed variables for the dynamic content:
{
"template": "order-confirmation",
"type": "transactional",
"variables": [
{ "name": "orderNumber", "type": "string", "required": true },
{ "name": "items", "type": "array", "required": true },
{ "name": "total", "type": "string", "required": true },
{ "name": "shippingAddress", "type": "string", "required": true },
{ "name": "estimatedDelivery", "type": "date", "required": false }
]
}
Validate that all required variables are present before attempting to render. A receipt with {{orderNumber}} literally in the text is worse than a slightly delayed send.
Transactional emails follow different suppression rules than marketing emails, but they're not exempt from all suppression.
Password change confirmations, new device alerts, and account compromise warnings should bypass nearly all suppression. A user whose account is being compromised needs that alert even if they previously reported your emails as spam. The only valid block is a hard bounce (the address literally doesn't exist).
This is the most common and most damaging mistake. A marketing campaign that triggers spam complaints tanks your transactional delivery. Password resets land in spam. Users can't log in. Support tickets flood in. Separate your streams.
A webhook fires twice, a queue job retries after a timeout, a microservice calls the send API from two replicas - and the customer gets two identical receipts. Always use dedupe keys for transactional sends.
Some teams label emails as "transactional" to skip unsubscribe links and CAN-SPAM compliance. The FTC sees through this. If the primary purpose is commercial, it's commercial regardless of what you label it. The penalty is up to $51,744 per email.
Password resets and 2FA codes that take minutes to arrive cause users to request new ones (multiplying your send volume) and eventually abandon the flow. If your auth emails aren't arriving in under 10 seconds, fix your queue priority and provider configuration.
A user who unsubscribed from your marketing emails should absolutely still receive their order receipt. Keep transactional suppression lists separate from marketing opt-out lists. The only overlap is hard bounces.
A receipt doesn't need a hero image, animated GIFs, and a social media footer. Heavy templates load slowly, trigger spam filters, and render inconsistently. Keep transactional templates clean and functional. Save the design effort for marketing.
Auth codes rendered only as images are inaccessible to screen readers, break in text-only clients, and fail when images are blocked (which many corporate email systems do by default). Always include critical information as plain text.
If the user didn't take an action that caused the email, it's probably not transactional. "We noticed you haven't logged in for a while" is marketing, not transactional, even if you frame it as an account alert.
data-ai
Choose and configure an email service provider. Use when setting up email for a new project, comparing providers, migrating between providers, or adding failover.
development
Set up SPF, DKIM, and DMARC email authentication. Use when configuring a new sending domain, debugging spam/rejection issues, adding email providers, or preparing for Google/Yahoo/Microsoft bulk sender requirements.
development
Build welcome and activation email sequences. Use when designing signup flows, driving users to key actions, converting trials to paid, or reducing early churn.
development
Design product notification emails. Use when building activity alerts, digests, status updates, or managing notification frequency and user preferences.