email skill
This commit is contained in:
59
profiles/opencode/skill/email-best-practices/SKILL.md
Normal file
59
profiles/opencode/skill/email-best-practices/SKILL.md
Normal file
@@ -0,0 +1,59 @@
|
||||
---
|
||||
name: email-best-practices
|
||||
description: Use when building email features, emails going to spam, high bounce rates, setting up SPF/DKIM/DMARC authentication, implementing email capture, ensuring compliance (CAN-SPAM, GDPR, CASL), handling webhooks, retry logic, or deciding transactional vs marketing.
|
||||
---
|
||||
|
||||
# Email Best Practices
|
||||
|
||||
Guidance for building deliverable, compliant, user-friendly emails.
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
```
|
||||
[User] → [Email Form] → [Validation] → [Double Opt-In]
|
||||
↓
|
||||
[Consent Recorded]
|
||||
↓
|
||||
[Suppression Check] ←──────────────[Ready to Send]
|
||||
↓
|
||||
[Idempotent Send + Retry] ──────→ [Email API]
|
||||
↓
|
||||
[Webhook Events]
|
||||
↓
|
||||
┌────────┬────────┬─────────────┐
|
||||
↓ ↓ ↓ ↓
|
||||
Delivered Bounced Complained Opened/Clicked
|
||||
↓ ↓
|
||||
[Suppression List Updated]
|
||||
↓
|
||||
[List Hygiene Jobs]
|
||||
```
|
||||
|
||||
## Quick Reference
|
||||
|
||||
| Need to... | See |
|
||||
|------------|-----|
|
||||
| Set up SPF/DKIM/DMARC, fix spam issues | [Deliverability](./resources/deliverability.md) |
|
||||
| Build password reset, OTP, confirmations | [Transactional Emails](./resources/transactional-emails.md) |
|
||||
| Plan which emails your app needs | [Transactional Email Catalog](./resources/transactional-email-catalog.md) |
|
||||
| Build newsletter signup, validate emails | [Email Capture](./resources/email-capture.md) |
|
||||
| Send newsletters, promotions | [Marketing Emails](./resources/marketing-emails.md) |
|
||||
| Ensure CAN-SPAM/GDPR/CASL compliance | [Compliance](./resources/compliance.md) |
|
||||
| Decide transactional vs marketing | [Email Types](./resources/email-types.md) |
|
||||
| Handle retries, idempotency, errors | [Sending Reliability](./resources/sending-reliability.md) |
|
||||
| Process delivery events, set up webhooks | [Webhooks & Events](./resources/webhooks-events.md) |
|
||||
| Manage bounces, complaints, suppression | [List Management](./resources/list-management.md) |
|
||||
|
||||
## Start Here
|
||||
|
||||
**New app?**
|
||||
Start with the [Catalog](./resources/transactional-email-catalog.md) to plan which emails your app needs (password reset, verification, etc.), then set up [Deliverability](./resources/deliverability.md) (DNS authentication) before sending your first email.
|
||||
|
||||
**Spam issues?**
|
||||
Check [Deliverability](./resources/deliverability.md) first—authentication problems are the most common cause. Gmail/Yahoo reject unauthenticated emails.
|
||||
|
||||
**Marketing emails?**
|
||||
Follow this path: [Email Capture](./resources/email-capture.md) (collect consent) → [Compliance](./resources/compliance.md) (legal requirements) → [Marketing Emails](./resources/marketing-emails.md) (best practices).
|
||||
|
||||
**Production-ready sending?**
|
||||
Add reliability: [Sending Reliability](./resources/sending-reliability.md) (retry + idempotency) → [Webhooks & Events](./resources/webhooks-events.md) (track delivery) → [List Management](./resources/list-management.md) (handle bounces).
|
||||
@@ -0,0 +1,103 @@
|
||||
# Email Compliance
|
||||
|
||||
Legal requirements for email by jurisdiction. **Not legal advice—consult an attorney for your specific situation.**
|
||||
|
||||
## Quick Reference
|
||||
|
||||
| Law | Region | Key Requirement | Penalty |
|
||||
|-----|--------|-----------------|---------|
|
||||
| CAN-SPAM | US | Opt-out mechanism, physical address | $53k/email |
|
||||
| GDPR | EU | Explicit opt-in consent | €20M or 4% revenue |
|
||||
| CASL | Canada | Express/implied consent | $10M CAD |
|
||||
|
||||
## CAN-SPAM (United States)
|
||||
|
||||
**Requirements:**
|
||||
- Accurate header info (From, To, Reply-To)
|
||||
- Non-deceptive subject lines
|
||||
- Physical mailing address in every email
|
||||
- Clear opt-out mechanism
|
||||
- Honor opt-out within 10 business days
|
||||
|
||||
**Transactional emails:** Can send without opt-in if related to a transaction and not promotional.
|
||||
|
||||
## GDPR (European Union)
|
||||
|
||||
**Requirements:**
|
||||
- Explicit opt-in consent (not pre-checked boxes)
|
||||
- Consent must be freely given, specific, informed
|
||||
- Easy to withdraw consent (as easy as giving it)
|
||||
- Right to access data and deletion ("right to be forgotten")
|
||||
- Process unsubscribe immediately
|
||||
|
||||
**Consent records:** Document who, when, how, and what they consented to.
|
||||
|
||||
**Transactional emails:** Can send based on contract fulfillment or legitimate interest.
|
||||
|
||||
## CASL (Canada)
|
||||
|
||||
**Consent types:**
|
||||
- **Express consent:** Explicit opt-in (preferred)
|
||||
- **Implied consent:** Existing business relationship (2 years) or inquiry (6 months)
|
||||
|
||||
**Requirements:**
|
||||
- Clear sender identification
|
||||
- Unsubscribe functional for 60 days after send
|
||||
- Process unsubscribe within 10 business days
|
||||
- Keep consent records 3 years after expiration
|
||||
|
||||
## Other Regions
|
||||
|
||||
| Region | Law | Key Points |
|
||||
|--------|-----|------------|
|
||||
| Australia | Spam Act 2003 | Consent required, honor unsubscribe within 5 days |
|
||||
| UK | PECR + GDPR | Same as GDPR |
|
||||
| Brazil | LGPD | Similar to GDPR, explicit consent for marketing |
|
||||
|
||||
## Unsubscribe Requirements Summary
|
||||
|
||||
| Law | Timing | Notes |
|
||||
|-----|--------|-------|
|
||||
| CAN-SPAM | 10 business days | Must work 30 days after send |
|
||||
| GDPR | Immediately | Must be as easy as opting in |
|
||||
| CASL | 10 business days | Must work 60 days after send |
|
||||
|
||||
**Universal best practices:** Prominent link, one-click when possible, no login required, free, confirm action.
|
||||
|
||||
## Consent Management
|
||||
|
||||
**Record:**
|
||||
- Email address
|
||||
- Date/time of consent
|
||||
- Method (form, checkbox)
|
||||
- What they consented to
|
||||
- Source (which page/form)
|
||||
|
||||
**Storage:** Database with timestamps, audit trail of changes, link to user account.
|
||||
|
||||
## Data Retention
|
||||
|
||||
| Law | Requirement |
|
||||
|-----|-------------|
|
||||
| GDPR | Keep only as long as necessary, delete when no longer needed |
|
||||
| CASL | Keep consent records 3 years after expiration |
|
||||
|
||||
**Best practice:** Have clear retention policy, honor deletion requests promptly, review and clean regularly.
|
||||
|
||||
## Privacy Policy Must Include
|
||||
|
||||
- What data you collect
|
||||
- How you use data
|
||||
- Who you share data with
|
||||
- User rights (access, deletion)
|
||||
- How to contact about privacy
|
||||
|
||||
## International Sending
|
||||
|
||||
**Best practice:** Follow the most restrictive requirements (usually GDPR) to ensure compliance across all regions.
|
||||
|
||||
## Related
|
||||
|
||||
- [Email Capture](./email-capture.md) - Implement consent forms and double opt-in
|
||||
- [Marketing Emails](./marketing-emails.md) - Consent and unsubscribe requirements
|
||||
- [List Management](./list-management.md) - Handle unsubscribes and deletion requests
|
||||
@@ -0,0 +1,120 @@
|
||||
# Email Deliverability
|
||||
|
||||
Ensuring emails reach inboxes through proper authentication and sender reputation.
|
||||
|
||||
## Email Authentication
|
||||
|
||||
**Required by Gmail/Yahoo** - unauthenticated emails will be rejected or spam-filtered.
|
||||
|
||||
### SPF (Sender Policy Framework)
|
||||
|
||||
Specifies which servers can send email for your domain.
|
||||
|
||||
```
|
||||
v=spf1 include:_spf.resend.com ~all
|
||||
```
|
||||
|
||||
- Add TXT record to DNS
|
||||
- Use `~all` (soft fail) for testing, `-all` (hard fail) for production
|
||||
- Keep under 10 DNS lookups
|
||||
|
||||
### DKIM (DomainKeys Identified Mail)
|
||||
|
||||
Cryptographic signature proving email authenticity.
|
||||
|
||||
- Generate keys (provided by email service)
|
||||
- Add public key as TXT record in DNS
|
||||
- Use 2048-bit keys, rotate every 6-12 months
|
||||
|
||||
### DMARC
|
||||
|
||||
Policy for handling SPF/DKIM failures + reporting.
|
||||
|
||||
```
|
||||
v=DMARC1; p=none; rua=mailto:dmarc@yourdomain.com
|
||||
```
|
||||
|
||||
**Rollout:** `p=none` (monitor) → `p=quarantine; pct=25` → `p=reject`
|
||||
|
||||
### BIMI (Optional)
|
||||
|
||||
Display brand logo in email clients. Requires DMARC `p=quarantine` or `p=reject`.
|
||||
|
||||
### Verify Your Setup
|
||||
|
||||
Check DNS records directly:
|
||||
|
||||
```bash
|
||||
# SPF record
|
||||
dig TXT yourdomain.com +short
|
||||
|
||||
# DKIM record (replace 'resend' with your selector)
|
||||
dig TXT resend._domainkey.yourdomain.com +short
|
||||
|
||||
# DMARC record
|
||||
dig TXT _dmarc.yourdomain.com +short
|
||||
```
|
||||
|
||||
**Expected output:** Each command should return your configured record. No output = record missing.
|
||||
|
||||
## Sender Reputation
|
||||
|
||||
### IP Warming
|
||||
|
||||
New IP/domain? Gradually increase volume:
|
||||
|
||||
| Week | Daily Volume |
|
||||
|------|-------------|
|
||||
| 1 | 50-100 |
|
||||
| 2 | 200-500 |
|
||||
| 3 | 1,000-2,000 |
|
||||
| 4 | 5,000-10,000 |
|
||||
|
||||
Start with engaged users. Send consistently. Don't rush.
|
||||
|
||||
### Maintaining Reputation
|
||||
|
||||
**Do:** Send to engaged users, keep bounce <2%, complaints <0.1%, remove inactive subscribers
|
||||
|
||||
**Don't:** Send to purchased lists, ignore bounces/complaints, send inconsistent volumes
|
||||
|
||||
## Bounce Handling
|
||||
|
||||
| Type | Cause | Action |
|
||||
|------|-------|--------|
|
||||
| Hard bounce | Invalid email, domain doesn't exist | Remove immediately |
|
||||
| Soft bounce | Mailbox full, server down | Retry: 1h → 4h → 24h, remove after 3-5 failures |
|
||||
|
||||
**Targets:** <2% good, 2-5% acceptable, >5% concerning, >10% critical
|
||||
|
||||
## Complaint Handling
|
||||
|
||||
**Targets:** <0.05% excellent, 0.05-0.1% good, >0.2% critical
|
||||
|
||||
**Reduce complaints:**
|
||||
- Only send to opted-in users
|
||||
- Make unsubscribe easy and immediate
|
||||
- Use clear sender names and "From" addresses
|
||||
|
||||
**Feedback loops:** Set up with Gmail (Postmaster Tools), Yahoo, Microsoft, AOL. Remove complainers immediately.
|
||||
|
||||
## Infrastructure
|
||||
|
||||
**Dedicated sending domain:** Use subdomain (e.g., `mail.yourdomain.com`) to protect main domain reputation.
|
||||
|
||||
**DNS TTL:** Low (300s) during setup, high (3600s+) after stable.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**Emails going to spam?** Check in order:
|
||||
1. Authentication (SPF, DKIM, DMARC)
|
||||
2. Sender reputation (blacklists, complaint rates)
|
||||
3. Content (spammy words, HTML issues)
|
||||
4. Sending patterns (sudden volume spikes)
|
||||
|
||||
**Diagnostic tools:** [mail-tester.com](https://mail-tester.com), [mxtoolbox.com](https://mxtoolbox.com), [Google Postmaster Tools](https://postmaster.google.com)
|
||||
|
||||
## Related
|
||||
|
||||
- [List Management](./list-management.md) - Handle bounces and complaints to protect reputation
|
||||
- [Sending Reliability](./sending-reliability.md) - Retry logic and error handling
|
||||
@@ -0,0 +1,126 @@
|
||||
# Email Capture Best Practices
|
||||
|
||||
Collecting email addresses responsibly with validation, verification, and proper consent.
|
||||
|
||||
## Email Validation
|
||||
|
||||
### Client-Side
|
||||
|
||||
**HTML5:**
|
||||
```html
|
||||
<input type="email" required>
|
||||
```
|
||||
|
||||
**Best practices:**
|
||||
- Validate on blur or with short debounce
|
||||
- Show clear error messages
|
||||
- Don't be too strict (allow unusual but valid formats)
|
||||
- Client-side validation ≠ deliverability
|
||||
|
||||
### Server-Side (Required)
|
||||
|
||||
Always validate server-side—client-side can be bypassed.
|
||||
|
||||
**Check:**
|
||||
- Email format (RFC 5322)
|
||||
- Domain exists (DNS lookup)
|
||||
- Domain has MX records
|
||||
- Optionally: disposable email detection
|
||||
|
||||
## Email Verification
|
||||
|
||||
Confirms address belongs to user and is deliverable.
|
||||
|
||||
### Process
|
||||
|
||||
1. User submits email
|
||||
2. Send verification email with unique link/token
|
||||
3. User clicks link
|
||||
4. Mark as verified
|
||||
5. Allow access/add to list
|
||||
|
||||
**Timing:** Send immediately, include expiration (24-48 hours), allow resend after 60 seconds, limit resend attempts (3/hour).
|
||||
|
||||
## Single vs Double Opt-In
|
||||
|
||||
| | Single Opt-In | Double Opt-In |
|
||||
|--|---------------|---------------|
|
||||
| **Process** | Add to list immediately | Require email confirmation first |
|
||||
| **Pros** | Lower friction, faster growth | Verified addresses, better engagement, meets GDPR/CASL |
|
||||
| **Cons** | Higher invalid rate, lower engagement | Some users don't confirm |
|
||||
| **Use for** | Account creation, transactional | Marketing lists, newsletters |
|
||||
|
||||
**Recommendation:** Double opt-in for all marketing emails.
|
||||
|
||||
## Form Design
|
||||
|
||||
### Email Input
|
||||
|
||||
- Use `type="email"` for mobile keyboard
|
||||
- Include placeholder ("you@example.com")
|
||||
- Clear error messages ("Please enter a valid email address" not "Invalid")
|
||||
|
||||
### Consent Checkboxes (Marketing)
|
||||
|
||||
- **Unchecked by default** (required)
|
||||
- Specific language about what they're signing up for
|
||||
- Separate checkboxes for different email types
|
||||
- Link to privacy policy
|
||||
|
||||
```
|
||||
☐ Subscribe to our weekly newsletter with product updates
|
||||
☐ Send me promotional offers and deals
|
||||
```
|
||||
|
||||
**Don't:** Pre-check boxes, use vague language, hide in terms.
|
||||
|
||||
### Form Layout
|
||||
|
||||
- Keep simple and focused
|
||||
- One primary action
|
||||
- Clear value proposition
|
||||
- Mobile-friendly
|
||||
- Accessible (labels, ARIA)
|
||||
|
||||
## Error Handling
|
||||
|
||||
### Invalid Email
|
||||
|
||||
- Show clear error message
|
||||
- Suggest corrections for common typos (@gmial.com → @gmail.com)
|
||||
- Allow user to fix and resubmit
|
||||
|
||||
### Already Registered
|
||||
|
||||
- Accounts: "This email is already registered. [Sign in]"
|
||||
- Marketing: "You're already subscribed! [Manage preferences]"
|
||||
- Don't reveal if account exists (security)
|
||||
|
||||
### Rate Limiting
|
||||
|
||||
- Limit verification emails (3/hour per email)
|
||||
- Rate limit form submissions
|
||||
- Use CAPTCHA sparingly if needed
|
||||
- Monitor for abuse patterns
|
||||
|
||||
## Verification Emails
|
||||
|
||||
**Content:**
|
||||
- Clear purpose ("Verify your email address")
|
||||
- Prominent verification button
|
||||
- Expiration time
|
||||
- Resend option
|
||||
- "I didn't request this" notice
|
||||
|
||||
**Design:**
|
||||
- Mobile-friendly
|
||||
- Large, tappable button
|
||||
- Clear call-to-action
|
||||
|
||||
See [Transactional Emails](./transactional-emails.md) for detailed email design guidance.
|
||||
|
||||
## Related
|
||||
|
||||
- [Compliance](./compliance.md) - Legal requirements for consent (GDPR, CASL)
|
||||
- [Marketing Emails](./marketing-emails.md) - What happens after capture
|
||||
- [Deliverability](./deliverability.md) - How validation improves sender reputation
|
||||
@@ -0,0 +1,177 @@
|
||||
# Email Types: Transactional vs Marketing
|
||||
|
||||
Understanding the difference between transactional and marketing emails is crucial for compliance, deliverability, and user experience. This guide explains the distinctions and provides a catalog of transactional emails your app should include.
|
||||
|
||||
## When to Use This
|
||||
|
||||
- Deciding whether an email should be transactional or marketing
|
||||
- Understanding legal distinctions between email types
|
||||
- Planning what transactional emails your app needs
|
||||
- Ensuring compliance with email regulations
|
||||
- Setting up separate sending infrastructure
|
||||
|
||||
## Transactional vs Marketing: Key Differences
|
||||
|
||||
### Transactional Emails
|
||||
|
||||
**Definition:** Emails that facilitate or confirm a transaction the user initiated or expects. They're directly related to an action the user took.
|
||||
|
||||
**Characteristics:**
|
||||
- User-initiated or expected
|
||||
- Time-sensitive and actionable
|
||||
- Required for the user to complete an action
|
||||
- Not promotional in nature
|
||||
- Can be sent without explicit opt-in (with limitations)
|
||||
|
||||
**Examples:**
|
||||
- Password reset links
|
||||
- Order confirmations
|
||||
- Account verification
|
||||
- OTP/2FA codes
|
||||
- Shipping notifications
|
||||
|
||||
### Marketing Emails
|
||||
|
||||
**Definition:** Emails sent for promotional, advertising, or informational purposes that are not directly related to a specific transaction.
|
||||
|
||||
**Characteristics:**
|
||||
- Promotional or informational content
|
||||
- Not time-sensitive to complete a transaction
|
||||
- Require explicit opt-in (consent)
|
||||
- Must include unsubscribe options
|
||||
- Subject to stricter compliance requirements
|
||||
|
||||
**Examples:**
|
||||
- Newsletters
|
||||
- Product announcements
|
||||
- Promotional offers
|
||||
- Company updates
|
||||
- Educational content
|
||||
|
||||
## Legal Distinctions
|
||||
|
||||
### CAN-SPAM Act (US)
|
||||
|
||||
**Transactional emails:**
|
||||
- Can be sent without opt-in
|
||||
- Must be related to a transaction
|
||||
- Cannot contain promotional content (with exceptions)
|
||||
- Must identify sender and provide contact information
|
||||
|
||||
**Marketing emails:**
|
||||
- Require opt-out mechanism (not opt-in in US)
|
||||
- Must include clear sender identification
|
||||
- Must include physical mailing address
|
||||
- Must honor opt-out requests within 10 business days
|
||||
|
||||
### GDPR (EU)
|
||||
|
||||
**Transactional emails:**
|
||||
- Can be sent based on legitimate interest or contract fulfillment
|
||||
- Must be necessary for service delivery
|
||||
- Cannot contain marketing content without consent
|
||||
|
||||
**Marketing emails:**
|
||||
- Require explicit opt-in consent
|
||||
- Must clearly state purpose of data collection
|
||||
- Must provide easy unsubscribe
|
||||
- Subject to data protection requirements
|
||||
|
||||
### CASL (Canada)
|
||||
|
||||
**Transactional emails:**
|
||||
- Can be sent without consent if related to ongoing business relationship
|
||||
- Must be factual and not promotional
|
||||
|
||||
**Marketing emails:**
|
||||
- Require express or implied consent
|
||||
- Must include unsubscribe mechanism
|
||||
- Must identify sender clearly
|
||||
|
||||
## When to Use Each Type
|
||||
|
||||
### Use Transactional When:
|
||||
|
||||
- User needs the email to complete an action
|
||||
- Email confirms a transaction or account change
|
||||
- Email provides security-related information
|
||||
- Email is expected based on user action
|
||||
- Content is time-sensitive and actionable
|
||||
|
||||
### Use Marketing When:
|
||||
|
||||
- Promoting products or services
|
||||
- Sending newsletters or updates
|
||||
- Sharing educational content
|
||||
- Announcing features or company news
|
||||
- Content is not required for a transaction
|
||||
|
||||
## Hybrid Emails: The Gray Area
|
||||
|
||||
Some emails mix transactional and marketing content. Be careful:
|
||||
|
||||
**Best practice:** Keep transactional and marketing separate. If you must include marketing in a transactional email:
|
||||
- Make transactional content primary
|
||||
- Keep marketing content minimal and clearly separated
|
||||
- Ensure transactional purpose is clear
|
||||
- Check local regulations (some regions prohibit this)
|
||||
|
||||
**Example of acceptable hybrid:**
|
||||
- Order confirmation (transactional) with a small "You might also like" section (marketing)
|
||||
|
||||
**Example of problematic hybrid:**
|
||||
- Newsletter (marketing) with a small order status update (transactional)
|
||||
|
||||
## Transactional Email Catalog
|
||||
|
||||
For a complete catalog of transactional emails and recommended combinations by app type, see [Transactional Email Catalog](./transactional-email-catalog.md).
|
||||
|
||||
**Quick reference - Essential emails for most apps:**
|
||||
1. **Email verification** - Required for account creation
|
||||
2. **Password reset** - Required for account recovery
|
||||
3. **Welcome email** - Good user experience
|
||||
|
||||
The catalog includes detailed guidance for:
|
||||
- Authentication-focused apps
|
||||
- Newsletter / content platforms
|
||||
- E-commerce / marketplaces
|
||||
- SaaS / subscription services
|
||||
- Financial / fintech apps
|
||||
- Social / community platforms
|
||||
- Developer tools / API platforms
|
||||
- Healthcare / HIPAA-compliant apps
|
||||
|
||||
## Sending Infrastructure
|
||||
|
||||
### Separate Infrastructure
|
||||
|
||||
**Best practice:** Use separate sending infrastructure for transactional and marketing emails.
|
||||
|
||||
**Benefits:**
|
||||
- Protect transactional deliverability
|
||||
- Different authentication domains
|
||||
- Independent reputation
|
||||
- Easier compliance management
|
||||
|
||||
**Implementation:**
|
||||
- Use different subdomains (e.g., `mail.app.com` for transactional, `news.app.com` for marketing)
|
||||
- Separate email service accounts or API keys
|
||||
- Different monitoring and alerting
|
||||
|
||||
### Email Service Considerations
|
||||
|
||||
Choose an email service that:
|
||||
- Provides reliable delivery for transactional emails
|
||||
- Offers separate sending domains
|
||||
- Has good API for programmatic sending
|
||||
- Provides webhooks for delivery events
|
||||
- Supports authentication setup (SPF, DKIM, DMARC)
|
||||
|
||||
Services like Resend are designed for transactional emails and provide the infrastructure and tools needed for reliable delivery.
|
||||
|
||||
## Related Topics
|
||||
|
||||
- [Transactional Emails](./transactional-emails.md) - Best practices for sending transactional emails
|
||||
- [Marketing Emails](./marketing-emails.md) - Best practices for marketing emails
|
||||
- [Compliance](./compliance.md) - Legal requirements for each email type
|
||||
- [Deliverability](./deliverability.md) - Ensuring transactional emails are delivered
|
||||
@@ -0,0 +1,157 @@
|
||||
# List Management
|
||||
|
||||
Maintaining clean email lists through suppression, hygiene, and data retention.
|
||||
|
||||
## Suppression Lists
|
||||
|
||||
A suppression list prevents sending to addresses that should never receive email.
|
||||
|
||||
### What to Suppress
|
||||
|
||||
| Reason | Action | Can Unsuppress? |
|
||||
|--------|--------|-----------------|
|
||||
| Hard bounce | Add immediately | No (address invalid) |
|
||||
| Complaint (spam) | Add immediately | No (legal requirement) |
|
||||
| Unsubscribe | Add immediately | Only if user re-subscribes |
|
||||
| Soft bounce (3x) | Add after threshold | Yes, after 30-90 days |
|
||||
| Manual removal | Add on request | Only if user requests |
|
||||
|
||||
### Implementation
|
||||
|
||||
```typescript
|
||||
// Suppression list schema
|
||||
interface SuppressionEntry {
|
||||
email: string;
|
||||
reason: 'hard_bounce' | 'complaint' | 'unsubscribe' | 'soft_bounce' | 'manual';
|
||||
created_at: Date;
|
||||
source_email_id?: string; // Which email triggered this
|
||||
}
|
||||
|
||||
// Check before every send
|
||||
async function canSendTo(email: string): Promise<boolean> {
|
||||
const suppressed = await db.suppressions.findOne({ email });
|
||||
return !suppressed;
|
||||
}
|
||||
|
||||
// Add to suppression list
|
||||
async function suppressEmail(email: string, reason: string, sourceId?: string) {
|
||||
await db.suppressions.upsert({
|
||||
email: email.toLowerCase(),
|
||||
reason,
|
||||
created_at: new Date(),
|
||||
source_email_id: sourceId,
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### Pre-Send Check
|
||||
|
||||
**Always check suppression before sending:**
|
||||
|
||||
```typescript
|
||||
async function sendEmail(to: string, emailData: EmailData) {
|
||||
if (!await canSendTo(to)) {
|
||||
console.log(`Skipping suppressed email: ${to}`);
|
||||
return { skipped: true, reason: 'suppressed' };
|
||||
}
|
||||
|
||||
return await resend.emails.send({ to, ...emailData });
|
||||
}
|
||||
```
|
||||
|
||||
## List Hygiene
|
||||
|
||||
Regular maintenance to keep lists healthy.
|
||||
|
||||
### Automated Cleanup
|
||||
|
||||
| Task | Frequency | Action |
|
||||
|------|-----------|--------|
|
||||
| Remove hard bounces | Real-time (via webhook) | Immediate suppression |
|
||||
| Remove complaints | Real-time (via webhook) | Immediate suppression |
|
||||
| Process unsubscribes | Real-time | Remove from marketing lists |
|
||||
| Review soft bounces | Daily | Suppress after 3 failures |
|
||||
| Remove inactive | Monthly | Re-engagement → remove |
|
||||
|
||||
### Re-engagement Campaigns
|
||||
|
||||
Before removing inactive subscribers:
|
||||
|
||||
1. **Identify inactive:** No opens/clicks in 90-180 days
|
||||
2. **Send re-engagement:** "We miss you" or "Still interested?"
|
||||
3. **Wait 14-30 days** for response
|
||||
4. **Remove non-responders** from active lists
|
||||
|
||||
```typescript
|
||||
async function runReengagement() {
|
||||
const inactive = await getInactiveSubscribers(90); // 90 days
|
||||
|
||||
for (const subscriber of inactive) {
|
||||
if (!subscriber.reengagement_sent) {
|
||||
await sendReengagementEmail(subscriber);
|
||||
await markReengagementSent(subscriber.email);
|
||||
} else if (daysSince(subscriber.reengagement_sent) > 30) {
|
||||
await removeFromMarketingLists(subscriber.email);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Data Retention
|
||||
|
||||
### Email Logs
|
||||
|
||||
| Data Type | Recommended Retention | Notes |
|
||||
|-----------|----------------------|-------|
|
||||
| Send attempts | 90 days | Debugging, analytics |
|
||||
| Delivery status | 90 days | Compliance, reporting |
|
||||
| Bounce/complaint events | 3 years | Required for CASL |
|
||||
| Suppression list | Indefinite | Never delete |
|
||||
| Email content | 30 days | Storage costs |
|
||||
| Consent records | 3 years after expiry | Legal requirement |
|
||||
|
||||
### Retention Policy Implementation
|
||||
|
||||
```typescript
|
||||
// Daily cleanup job
|
||||
async function cleanupOldData() {
|
||||
const now = new Date();
|
||||
|
||||
// Delete old email logs (keep 90 days)
|
||||
await db.emailLogs.deleteMany({
|
||||
created_at: { $lt: subDays(now, 90) }
|
||||
});
|
||||
|
||||
// Delete old email content (keep 30 days)
|
||||
await db.emailContent.deleteMany({
|
||||
created_at: { $lt: subDays(now, 30) }
|
||||
});
|
||||
|
||||
// Never delete: suppressions, consent records
|
||||
}
|
||||
```
|
||||
|
||||
## Metrics to Monitor
|
||||
|
||||
| Metric | Target | Alert Threshold |
|
||||
|--------|--------|-----------------|
|
||||
| Bounce rate | <2% | >5% |
|
||||
| Complaint rate | <0.1% | >0.2% |
|
||||
| Suppression list growth | Stable | Sudden spike |
|
||||
| List churn | <2%/month | >5%/month |
|
||||
|
||||
## Transactional vs Marketing Lists
|
||||
|
||||
**Keep separate:**
|
||||
- Transactional: Can send to anyone with account relationship
|
||||
- Marketing: Only opted-in subscribers
|
||||
|
||||
**Suppression applies to both:** Hard bounces and complaints suppress across all email types.
|
||||
|
||||
**Unsubscribe is marketing-only:** User unsubscribing from marketing can still receive transactional emails (password resets, order confirmations).
|
||||
|
||||
## Related
|
||||
|
||||
- [Webhooks & Events](./webhooks-events.md) - Receive bounce/complaint notifications
|
||||
- [Deliverability](./deliverability.md) - How list hygiene affects sender reputation
|
||||
- [Compliance](./compliance.md) - Legal requirements for data retention
|
||||
@@ -0,0 +1,115 @@
|
||||
# Marketing Email Best Practices
|
||||
|
||||
Promotional emails that require explicit consent and provide value to recipients.
|
||||
|
||||
## Core Principles
|
||||
|
||||
1. **Consent first** - Explicit opt-in required (especially GDPR/CASL)
|
||||
2. **Value-driven** - Provide useful content, not just promotions
|
||||
3. **Respect preferences** - Let users control frequency and content types
|
||||
|
||||
## Opt-In Requirements
|
||||
|
||||
### Explicit Opt-In
|
||||
|
||||
**What counts:**
|
||||
- User checks unchecked box
|
||||
- User clicks "Subscribe" button
|
||||
- User completes form with clear subscription intent
|
||||
|
||||
**What doesn't count:**
|
||||
- Pre-checked boxes
|
||||
- Opt-out model
|
||||
- Assumed consent from purchase
|
||||
- Purchased/rented lists
|
||||
|
||||
### Informed Consent
|
||||
|
||||
Disclose: email types, frequency, sender identity, how to unsubscribe.
|
||||
|
||||
✅ "Subscribe to our weekly newsletter with product updates and tips"
|
||||
❌ "Sign up for emails"
|
||||
|
||||
### Double Opt-In (Recommended)
|
||||
|
||||
1. User submits email
|
||||
2. Send confirmation email with verification link
|
||||
3. User clicks to confirm
|
||||
4. Add to list only after confirmation
|
||||
|
||||
Benefits: Verifies deliverability, confirms intent, reduces complaints, required in some regions (Germany).
|
||||
|
||||
## Unsubscribe Requirements
|
||||
|
||||
**Must be:**
|
||||
- Prominent in every email
|
||||
- One-click (preferred) or simple process
|
||||
- Immediate (GDPR) or within 10 days (CAN-SPAM)
|
||||
- Free, no login required
|
||||
|
||||
**Preference center options:** Frequency (daily/weekly/monthly), content types, complete unsubscribe.
|
||||
|
||||
## Content and Design
|
||||
|
||||
### Subject Lines
|
||||
|
||||
- Clear and specific (50 chars or less for mobile)
|
||||
- Create curiosity without misleading
|
||||
- A/B test regularly
|
||||
|
||||
✅ "Your weekly digest: 5 productivity tips"
|
||||
❌ "You won't believe what happened!"
|
||||
|
||||
### Structure
|
||||
|
||||
**Above fold:** Value proposition, primary CTA, engaging visual
|
||||
|
||||
**Body:** Scannable (short paragraphs, bullets), clear hierarchy, multiple CTAs
|
||||
|
||||
**Footer:** Unsubscribe link, company info, physical address (CAN-SPAM), social links
|
||||
|
||||
### Mobile-First
|
||||
|
||||
- Single column layout
|
||||
- 44x44px minimum buttons
|
||||
- 16px minimum text
|
||||
- Test on iOS, Android, dark mode
|
||||
|
||||
## Segmentation
|
||||
|
||||
**Segment by:** Behavior (purchases, activity), demographics, preferences, engagement level, signup source.
|
||||
|
||||
Benefits: Higher open/click rates, lower unsubscribes, better experience.
|
||||
|
||||
## Personalization
|
||||
|
||||
**Options:** Name in subject/greeting, location-specific content, behavior-based recommendations, purchase history.
|
||||
|
||||
**Don't over-personalize** - can feel intrusive. Use data you have permission to use.
|
||||
|
||||
## Frequency and Timing
|
||||
|
||||
**Frequency:** Start conservative, increase based on engagement, let users set preferences, monitor unsubscribe rates.
|
||||
|
||||
**Timing:** Weekday mornings (9-11 AM local), Tuesday-Thursday often best. Test your specific audience.
|
||||
|
||||
## List Hygiene
|
||||
|
||||
**Remove immediately:** Hard bounces, unsubscribes, complaints
|
||||
|
||||
**Remove after inactivity:** Send re-engagement campaign first, then remove non-responders
|
||||
|
||||
**Monitor:** Bounce rate <2%, complaint rate <0.1%
|
||||
|
||||
## Required Elements (All Marketing Emails)
|
||||
|
||||
- Clear sender identification
|
||||
- Physical mailing address (CAN-SPAM)
|
||||
- Unsubscribe mechanism
|
||||
- Indication it's marketing (GDPR)
|
||||
|
||||
## Related
|
||||
|
||||
- [Compliance](./compliance.md) - Detailed legal requirements by region
|
||||
- [Email Capture](./email-capture.md) - Collecting consent properly
|
||||
- [List Management](./list-management.md) - Maintaining list hygiene
|
||||
@@ -0,0 +1,155 @@
|
||||
# Sending Reliability
|
||||
|
||||
Ensuring emails are sent exactly once and handling failures gracefully.
|
||||
|
||||
## Idempotency
|
||||
|
||||
Prevent duplicate emails when retrying failed requests.
|
||||
|
||||
### The Problem
|
||||
|
||||
Network issues, timeouts, or server errors can leave you uncertain if an email was sent. Retrying without idempotency risks sending duplicates.
|
||||
|
||||
### Solution: Idempotency Keys
|
||||
|
||||
Send a unique key with each request. If the same key is sent again, the server returns the original response instead of sending another email.
|
||||
|
||||
```typescript
|
||||
// Generate deterministic key based on the business event
|
||||
const idempotencyKey = `password-reset-${userId}-${resetRequestId}`;
|
||||
|
||||
await resend.emails.send({
|
||||
from: 'noreply@example.com',
|
||||
to: user.email,
|
||||
subject: 'Reset your password',
|
||||
html: emailHtml,
|
||||
}, {
|
||||
headers: {
|
||||
'Idempotency-Key': idempotencyKey
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### Key Generation Strategies
|
||||
|
||||
| Strategy | Example | Use When |
|
||||
|----------|---------|----------|
|
||||
| Event-based | `order-confirm-${orderId}` | One email per event (recommended) |
|
||||
| Request-scoped | `reset-${userId}-${resetRequestId}` | Retries within same request |
|
||||
| UUID | `crypto.randomUUID()` | No natural key (generate once, reuse on retry) |
|
||||
|
||||
**Best practice:** Use deterministic keys based on the business event. If you retry the same logical send, the same key must be generated. Avoid `Date.now()` or random values generated fresh on each attempt.
|
||||
|
||||
**Key expiration:** Idempotency keys are typically cached for 24 hours. Retries within this window return the original response. After expiration, the same key triggers a new send—so complete your retry logic well within 24 hours.
|
||||
|
||||
## Retry Logic
|
||||
|
||||
Handle transient failures with exponential backoff.
|
||||
|
||||
### When to Retry
|
||||
|
||||
| Error Type | Retry? | Notes |
|
||||
|------------|--------|-------|
|
||||
| 5xx (server error) | ✅ Yes | Transient, likely to resolve |
|
||||
| 429 (rate limit) | ✅ Yes | Wait for rate limit window |
|
||||
| 4xx (client error) | ❌ No | Fix the request first |
|
||||
| Network timeout | ✅ Yes | Transient |
|
||||
| DNS failure | ✅ Yes | May be transient |
|
||||
|
||||
### Exponential Backoff
|
||||
|
||||
```typescript
|
||||
async function sendWithRetry(emailData, maxRetries = 3) {
|
||||
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
||||
try {
|
||||
return await resend.emails.send(emailData);
|
||||
} catch (error) {
|
||||
if (!isRetryable(error) || attempt === maxRetries - 1) {
|
||||
throw error;
|
||||
}
|
||||
const delay = Math.min(1000 * Math.pow(2, attempt), 30000);
|
||||
await sleep(delay + Math.random() * 1000); // Add jitter
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function isRetryable(error) {
|
||||
return error.statusCode >= 500 ||
|
||||
error.statusCode === 429 ||
|
||||
error.code === 'ETIMEDOUT';
|
||||
}
|
||||
```
|
||||
|
||||
**Backoff schedule:** 1s → 2s → 4s → 8s (with jitter to prevent thundering herd)
|
||||
|
||||
## Error Handling
|
||||
|
||||
### Common Error Codes
|
||||
|
||||
| Code | Meaning | Action |
|
||||
|------|---------|--------|
|
||||
| 400 | Bad request | Fix payload (invalid email, missing field) |
|
||||
| 401 | Unauthorized | Check API key |
|
||||
| 403 | Forbidden | Check permissions, domain verification |
|
||||
| 404 | Not found | Check endpoint URL |
|
||||
| 422 | Validation error | Fix request data |
|
||||
| 429 | Rate limited | Back off, retry after delay |
|
||||
| 500 | Server error | Retry with backoff |
|
||||
| 503 | Service unavailable | Retry with backoff |
|
||||
|
||||
### Error Handling Pattern
|
||||
|
||||
```typescript
|
||||
try {
|
||||
const result = await resend.emails.send(emailData);
|
||||
await logSuccess(result.id, emailData);
|
||||
} catch (error) {
|
||||
if (error.statusCode === 429) {
|
||||
await queueForRetry(emailData, error.retryAfter);
|
||||
} else if (error.statusCode >= 500) {
|
||||
await queueForRetry(emailData);
|
||||
} else {
|
||||
await logFailure(error, emailData);
|
||||
await alertOnCriticalEmail(emailData); // For password resets, etc.
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Queuing for Reliability
|
||||
|
||||
For critical emails, use a queue to ensure delivery even if the initial send fails.
|
||||
|
||||
**Benefits:**
|
||||
- Survives application restarts
|
||||
- Automatic retry handling
|
||||
- Rate limit management
|
||||
- Audit trail
|
||||
|
||||
**Simple pattern:**
|
||||
1. Write email to queue/database with "pending" status
|
||||
2. Process queue, attempt send
|
||||
3. On success: mark "sent", store message ID
|
||||
4. On retryable failure: increment retry count, schedule retry
|
||||
5. On permanent failure: mark "failed", alert
|
||||
|
||||
## Timeouts
|
||||
|
||||
Set appropriate timeouts to avoid hanging requests.
|
||||
|
||||
```typescript
|
||||
const controller = new AbortController();
|
||||
const timeout = setTimeout(() => controller.abort(), 10000);
|
||||
|
||||
try {
|
||||
await resend.emails.send(emailData, { signal: controller.signal });
|
||||
} finally {
|
||||
clearTimeout(timeout);
|
||||
}
|
||||
```
|
||||
|
||||
**Recommended:** 10-30 seconds for email API calls.
|
||||
|
||||
## Related
|
||||
|
||||
- [Webhooks & Events](./webhooks-events.md) - Process delivery confirmations and failures
|
||||
- [List Management](./list-management.md) - Handle bounces and suppress invalid addresses
|
||||
@@ -0,0 +1,418 @@
|
||||
# Transactional Email Catalog
|
||||
|
||||
A comprehensive catalog of transactional emails organized by category, plus recommended email combinations for different app types.
|
||||
|
||||
## When to Use This
|
||||
|
||||
- Planning what transactional emails your app needs
|
||||
- Choosing the right emails for your app type
|
||||
- Understanding what content each email type should include
|
||||
- Implementing transactional email features
|
||||
|
||||
## Email Combinations by App Type
|
||||
|
||||
Use these combinations as a starting point based on what you're building.
|
||||
|
||||
### Authentication-Focused App
|
||||
|
||||
Apps where user accounts and security are core (login systems, identity providers, account management).
|
||||
|
||||
**Essential:**
|
||||
- Email verification
|
||||
- Password reset
|
||||
- OTP / 2FA codes
|
||||
- Security alerts (new device, password change)
|
||||
- Account update notifications
|
||||
|
||||
**Optional:**
|
||||
- Welcome email
|
||||
- Account deletion confirmation
|
||||
|
||||
### Newsletter / Content Platform
|
||||
|
||||
Apps focused on content delivery and subscriptions.
|
||||
|
||||
**Essential:**
|
||||
- Email verification
|
||||
- Password reset
|
||||
- Welcome email
|
||||
- Subscription confirmation
|
||||
|
||||
**Optional:**
|
||||
- OTP / 2FA codes
|
||||
- Account update notifications
|
||||
|
||||
### E-commerce / Marketplace
|
||||
|
||||
Apps where users buy products or services.
|
||||
|
||||
**Essential:**
|
||||
- Email verification
|
||||
- Password reset
|
||||
- Welcome email
|
||||
- Order confirmation
|
||||
- Shipping notifications
|
||||
- Invoice / receipt
|
||||
- Payment failed notices
|
||||
|
||||
**Optional:**
|
||||
- OTP / 2FA codes
|
||||
- Security alerts
|
||||
- Subscription confirmations (for recurring orders)
|
||||
|
||||
### SaaS / Subscription Service
|
||||
|
||||
Apps with paid subscription tiers and ongoing billing.
|
||||
|
||||
**Essential:**
|
||||
- Email verification
|
||||
- Password reset
|
||||
- Welcome email
|
||||
- OTP / 2FA codes
|
||||
- Security alerts
|
||||
- Subscription confirmation
|
||||
- Subscription renewal notice
|
||||
- Payment failed notices
|
||||
- Invoice / receipt
|
||||
|
||||
**Optional:**
|
||||
- Account update notifications
|
||||
- Feature change notifications (for breaking changes)
|
||||
|
||||
### Financial / Fintech App
|
||||
|
||||
Apps handling money, payments, or sensitive financial data.
|
||||
|
||||
**Essential:**
|
||||
- Email verification
|
||||
- Password reset
|
||||
- OTP / 2FA codes (required for sensitive actions)
|
||||
- Security alerts (all types)
|
||||
- Account update notifications
|
||||
- Transaction confirmations
|
||||
- Invoice / receipt
|
||||
- Payment failed notices
|
||||
|
||||
**Optional:**
|
||||
- Welcome email
|
||||
- Compliance notices
|
||||
|
||||
### Social / Community Platform
|
||||
|
||||
Apps focused on user interaction and community features.
|
||||
|
||||
**Essential:**
|
||||
- Email verification
|
||||
- Password reset
|
||||
- Welcome email
|
||||
- Security alerts
|
||||
|
||||
**Optional:**
|
||||
- OTP / 2FA codes
|
||||
- Account update notifications
|
||||
- Activity notifications (mentions, replies)
|
||||
|
||||
### Developer Tools / API Platform
|
||||
|
||||
Apps targeting developers with API access and integrations.
|
||||
|
||||
**Essential:**
|
||||
- Email verification
|
||||
- Password reset
|
||||
- OTP / 2FA codes
|
||||
- Security alerts
|
||||
- API key notifications (creation, expiration)
|
||||
- Subscription confirmation
|
||||
- Payment failed notices
|
||||
|
||||
**Optional:**
|
||||
- Welcome email
|
||||
- Usage alerts (approaching limits)
|
||||
- Feature change notifications
|
||||
|
||||
### Healthcare / HIPAA-Compliant App
|
||||
|
||||
Apps handling protected health information.
|
||||
|
||||
**Essential:**
|
||||
- Email verification
|
||||
- Password reset
|
||||
- OTP / 2FA codes (required)
|
||||
- Security alerts (all types, detailed)
|
||||
- Account update notifications
|
||||
- Appointment confirmations
|
||||
|
||||
**Optional:**
|
||||
- Welcome email
|
||||
- Compliance notices
|
||||
|
||||
**Note:** Healthcare apps have strict requirements. Emails should contain minimal PHI and link to secure portals for sensitive information.
|
||||
|
||||
---
|
||||
|
||||
## Full Email Catalog
|
||||
|
||||
### Authentication & Security
|
||||
|
||||
#### Email Verification / Account Verification
|
||||
|
||||
**When to send:** Immediately after user signs up or changes email address.
|
||||
|
||||
**Purpose:** Verify the email address belongs to the user.
|
||||
|
||||
**Content should include:**
|
||||
- Clear verification link or code
|
||||
- Expiration time (typically 24-48 hours)
|
||||
- Instructions on what to do
|
||||
- Security notice if link is clicked by mistake
|
||||
|
||||
**Best practices:**
|
||||
- Send immediately (within seconds)
|
||||
- Include expiration notice
|
||||
- Provide resend option
|
||||
- Link to support if issues
|
||||
|
||||
#### OTP / 2FA Codes
|
||||
|
||||
**When to send:** When user requests two-factor authentication code.
|
||||
|
||||
**Purpose:** Provide time-sensitive authentication code.
|
||||
|
||||
**Content should include:**
|
||||
- The OTP code (clearly displayed)
|
||||
- Expiration time (typically 5-10 minutes)
|
||||
- Security warnings
|
||||
- Instructions on what to do if not requested
|
||||
|
||||
**Best practices:**
|
||||
- Send immediately
|
||||
- Code should be large and easy to read
|
||||
- Include expiration prominently
|
||||
- Warn about sharing codes
|
||||
- Provide "I didn't request this" link
|
||||
|
||||
#### Password Reset
|
||||
|
||||
**When to send:** When user requests password reset.
|
||||
|
||||
**Purpose:** Allow user to securely reset forgotten password.
|
||||
|
||||
**Content should include:**
|
||||
- Reset link (with token)
|
||||
- Expiration time (typically 1 hour)
|
||||
- Security warnings
|
||||
- Instructions if not requested
|
||||
|
||||
**Best practices:**
|
||||
- Send immediately
|
||||
- Link expires quickly (1 hour)
|
||||
- Include IP address and location if available
|
||||
- Provide "I didn't request this" link
|
||||
- Don't include the old password
|
||||
|
||||
#### Security Alerts
|
||||
|
||||
**When to send:** When security-relevant events occur (login from new device, password change, etc.).
|
||||
|
||||
**Purpose:** Notify user of account security events.
|
||||
|
||||
**Content should include:**
|
||||
- What happened (clear description)
|
||||
- When it happened
|
||||
- Location/IP if available
|
||||
- Action to take if suspicious
|
||||
- Link to security settings
|
||||
|
||||
**Best practices:**
|
||||
- Send immediately
|
||||
- Be clear and specific
|
||||
- Include actionable steps
|
||||
- Provide way to report suspicious activity
|
||||
|
||||
### Account Management
|
||||
|
||||
#### Welcome Email
|
||||
|
||||
**When to send:** Immediately after successful account creation and verification.
|
||||
|
||||
**Purpose:** Welcome new users and guide them to next steps.
|
||||
|
||||
**Content should include:**
|
||||
- Welcome message
|
||||
- Key features or next steps
|
||||
- Links to important resources
|
||||
- Support contact information
|
||||
|
||||
**Best practices:**
|
||||
- Send after email verification
|
||||
- Keep it focused and actionable
|
||||
- Don't overwhelm with information
|
||||
- Set expectations about future emails
|
||||
|
||||
#### Account Update Notifications
|
||||
|
||||
**When to send:** When user changes account settings (email, password, profile, etc.).
|
||||
|
||||
**Purpose:** Confirm account changes and provide security notice.
|
||||
|
||||
**Content should include:**
|
||||
- What changed
|
||||
- When it changed
|
||||
- Action to take if unauthorized
|
||||
- Link to account settings
|
||||
|
||||
**Best practices:**
|
||||
- Send immediately after change
|
||||
- Be specific about what changed
|
||||
- Include security notice
|
||||
- Provide easy way to revert if needed
|
||||
|
||||
### E-commerce & Transactions
|
||||
|
||||
#### Order Confirmations
|
||||
|
||||
**When to send:** Immediately after order is placed.
|
||||
|
||||
**Purpose:** Confirm order details and provide receipt.
|
||||
|
||||
**Content should include:**
|
||||
- Order number
|
||||
- Items ordered with quantities
|
||||
- Pricing breakdown
|
||||
- Shipping address
|
||||
- Estimated delivery date
|
||||
- Order tracking link (if available)
|
||||
|
||||
**Best practices:**
|
||||
- Send within minutes of order
|
||||
- Include all order details
|
||||
- Make it easy to print or save
|
||||
- Provide customer service contact
|
||||
|
||||
#### Shipping Notifications
|
||||
|
||||
**When to send:** When order ships, with tracking updates.
|
||||
|
||||
**Purpose:** Notify user that order has shipped and provide tracking.
|
||||
|
||||
**Content should include:**
|
||||
- Order number
|
||||
- Tracking number
|
||||
- Carrier information
|
||||
- Expected delivery date
|
||||
- Tracking link
|
||||
- Shipping address confirmation
|
||||
|
||||
**Best practices:**
|
||||
- Send when order ships
|
||||
- Include tracking number prominently
|
||||
- Provide carrier tracking link
|
||||
- Update on major tracking milestones
|
||||
|
||||
#### Invoices and Receipts
|
||||
|
||||
**When to send:** After payment is processed.
|
||||
|
||||
**Purpose:** Provide payment confirmation and receipt.
|
||||
|
||||
**Content should include:**
|
||||
- Invoice/receipt number
|
||||
- Payment amount
|
||||
- Payment method
|
||||
- Items/services purchased
|
||||
- Payment date
|
||||
- Downloadable PDF (if applicable)
|
||||
|
||||
**Best practices:**
|
||||
- Send immediately after payment
|
||||
- Include all payment details
|
||||
- Make it easy to download/save
|
||||
- Include tax information if applicable
|
||||
|
||||
### Subscriptions & Billing
|
||||
|
||||
#### Subscription Confirmations
|
||||
|
||||
**When to send:** When user subscribes or changes subscription.
|
||||
|
||||
**Purpose:** Confirm subscription details and billing information.
|
||||
|
||||
**Content should include:**
|
||||
- Subscription plan details
|
||||
- Billing amount and frequency
|
||||
- Next billing date
|
||||
- Payment method
|
||||
- Link to manage subscription
|
||||
|
||||
**Best practices:**
|
||||
- Send immediately after subscription
|
||||
- Clearly state billing terms
|
||||
- Provide easy cancellation option
|
||||
- Include support contact
|
||||
|
||||
#### Subscription Renewal Notices
|
||||
|
||||
**When to send:** Before subscription renews (typically 3-7 days before).
|
||||
|
||||
**Purpose:** Notify user of upcoming renewal and charge.
|
||||
|
||||
**Content should include:**
|
||||
- Renewal date
|
||||
- Amount to be charged
|
||||
- Payment method on file
|
||||
- Link to update payment method
|
||||
- Link to cancel if desired
|
||||
|
||||
**Best practices:**
|
||||
- Send with enough notice (3-7 days)
|
||||
- Be clear about amount and date
|
||||
- Make it easy to update payment method
|
||||
- Provide cancellation option
|
||||
|
||||
#### Payment Failed Notices
|
||||
|
||||
**When to send:** When subscription payment fails.
|
||||
|
||||
**Purpose:** Notify user of payment failure and provide resolution steps.
|
||||
|
||||
**Content should include:**
|
||||
- What happened
|
||||
- Amount that failed
|
||||
- Reason for failure (if available)
|
||||
- Steps to resolve
|
||||
- Link to update payment method
|
||||
- Consequences if not resolved
|
||||
|
||||
**Best practices:**
|
||||
- Send immediately after failure
|
||||
- Be clear about consequences
|
||||
- Provide easy resolution path
|
||||
- Include support contact
|
||||
|
||||
### Notifications & Updates
|
||||
|
||||
#### Feature Announcements (Transactional)
|
||||
|
||||
**When to send:** When a feature the user is using changes significantly.
|
||||
|
||||
**Purpose:** Notify users of changes that affect their use of the service.
|
||||
|
||||
**Content should include:**
|
||||
- What changed
|
||||
- How it affects the user
|
||||
- What action (if any) is needed
|
||||
- Link to more information
|
||||
|
||||
**Best practices:**
|
||||
- Only for significant changes
|
||||
- Focus on user impact
|
||||
- Provide clear next steps
|
||||
- Link to documentation
|
||||
|
||||
**Note:** General feature announcements are marketing emails. Only send as transactional if the change directly affects an active feature the user is using.
|
||||
|
||||
## Related Topics
|
||||
|
||||
- [Email Types](./email-types.md) - Understanding transactional vs marketing
|
||||
- [Transactional Emails](./transactional-emails.md) - Best practices for sending transactional emails
|
||||
- [Compliance](./compliance.md) - Legal requirements for each email type
|
||||
@@ -0,0 +1,92 @@
|
||||
# Transactional Email Best Practices
|
||||
|
||||
Clear, actionable emails that users expect and need—password resets, confirmations, OTPs.
|
||||
|
||||
## Core Principles
|
||||
|
||||
1. **Clarity over creativity** - Users need to understand and act quickly
|
||||
2. **Action-oriented** - Clear purpose, obvious primary action
|
||||
3. **Time-sensitive** - Send immediately (within seconds)
|
||||
|
||||
## Subject Lines
|
||||
|
||||
**Be specific and include context:**
|
||||
|
||||
| ✅ Good | ❌ Bad |
|
||||
|---------|--------|
|
||||
| Reset your password for [App] | Action required |
|
||||
| Your order #12345 has shipped | Update on your order |
|
||||
| Your 2FA code: 123456 | Security code |
|
||||
| Verify your email for [App] | Verify your email |
|
||||
|
||||
Include identifiers when helpful: order numbers, account names, expiration times.
|
||||
|
||||
## Pre-Header
|
||||
|
||||
The text snippet after subject line. Use it to:
|
||||
- Reinforce subject ("This link expires in 1 hour")
|
||||
- Add urgency or context
|
||||
- Call-to-action preview
|
||||
|
||||
Keep under 90-100 characters.
|
||||
|
||||
## Content Structure
|
||||
|
||||
**Above the fold (first screen):**
|
||||
- Clear purpose
|
||||
- Primary action button
|
||||
- Time-sensitive details (expiration)
|
||||
|
||||
**Hierarchy:** Header → Primary message → Details → Action button → Secondary info
|
||||
|
||||
**Format:** Short paragraphs (2-3 sentences), bullet points, bold for emphasis, white space.
|
||||
|
||||
## Mobile-First Design
|
||||
|
||||
60%+ emails opened on mobile.
|
||||
|
||||
- **Layout:** Single column, stack vertically
|
||||
- **Buttons:** 44x44px minimum, full-width on mobile
|
||||
- **Text:** 16px minimum body, 20-24px headings
|
||||
- **OTP codes:** 24-32px, monospace font
|
||||
|
||||
## Sender Configuration
|
||||
|
||||
| Field | Best Practice | Example |
|
||||
|-------|--------------|---------|
|
||||
| From Name | App/company name, consistent | [App Name] |
|
||||
| From Email | Subdomain, real address | hello@mail.yourdomain.com |
|
||||
| Reply-To | Monitored inbox | support@yourdomain.com |
|
||||
|
||||
Avoid `noreply@` - users reply to transactional emails.
|
||||
|
||||
## Code and Link Display
|
||||
|
||||
**OTP/Verification codes:**
|
||||
- Large (24-32px), monospace font
|
||||
- Centered, clear label
|
||||
- Include expiration nearby
|
||||
- Make copyable
|
||||
|
||||
**Buttons:**
|
||||
- Large, tappable (44x44px+)
|
||||
- Contrasting colors
|
||||
- Clear action text ("Reset Password", "Verify Email")
|
||||
- HTTPS links only
|
||||
|
||||
## Error Handling
|
||||
|
||||
**Resend functionality:**
|
||||
- Allow after 60 seconds
|
||||
- Limit attempts (3 per hour)
|
||||
- Show countdown timer
|
||||
|
||||
**Expired links:**
|
||||
- Clear "expired" message
|
||||
- Offer to send new link
|
||||
- Provide support contact
|
||||
|
||||
**"I didn't request this":**
|
||||
- Include in password resets, OTPs, security alerts
|
||||
- Link to security contact
|
||||
- Log clicks for monitoring
|
||||
@@ -0,0 +1,163 @@
|
||||
# Webhooks and Events
|
||||
|
||||
Receiving and processing email delivery events in real-time.
|
||||
|
||||
## Event Types
|
||||
|
||||
| Event | When Fired | Use For |
|
||||
|-------|------------|---------|
|
||||
| `email.sent` | Email accepted by Resend | Confirming send initiated |
|
||||
| `email.delivered` | Email delivered to recipient server | Confirming delivery |
|
||||
| `email.bounced` | Email bounced (hard or soft) | List hygiene, alerting |
|
||||
| `email.complained` | Recipient marked as spam | Immediate unsubscribe |
|
||||
| `email.opened` | Recipient opened email | Engagement tracking |
|
||||
| `email.clicked` | Recipient clicked link | Engagement tracking |
|
||||
|
||||
## Webhook Setup
|
||||
|
||||
### 1. Create Endpoint
|
||||
|
||||
Your endpoint must:
|
||||
- Accept POST requests
|
||||
- Return 2xx status quickly (within 5 seconds)
|
||||
- Handle duplicate events (idempotent processing)
|
||||
|
||||
```typescript
|
||||
app.post('/webhooks/resend', async (req, res) => {
|
||||
// Return 200 immediately to acknowledge receipt
|
||||
res.status(200).send('OK');
|
||||
|
||||
// Process asynchronously
|
||||
processWebhookAsync(req.body).catch(console.error);
|
||||
});
|
||||
```
|
||||
|
||||
### 2. Verify Signatures
|
||||
|
||||
Always verify webhook signatures to prevent spoofing.
|
||||
|
||||
```typescript
|
||||
import { Webhook } from 'svix';
|
||||
|
||||
const webhook = new Webhook(process.env.RESEND_WEBHOOK_SECRET);
|
||||
|
||||
app.post('/webhooks/resend', (req, res) => {
|
||||
try {
|
||||
const payload = webhook.verify(
|
||||
JSON.stringify(req.body),
|
||||
{
|
||||
'svix-id': req.headers['svix-id'],
|
||||
'svix-timestamp': req.headers['svix-timestamp'],
|
||||
'svix-signature': req.headers['svix-signature'],
|
||||
}
|
||||
);
|
||||
// Process verified payload
|
||||
} catch (err) {
|
||||
return res.status(400).send('Invalid signature');
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### 3. Register Webhook URL
|
||||
|
||||
Configure your webhook endpoint in the Resend dashboard or via API.
|
||||
|
||||
## Processing Events
|
||||
|
||||
### Bounce Handling
|
||||
|
||||
```typescript
|
||||
async function handleBounce(event) {
|
||||
const { email_id, email, bounce_type } = event.data;
|
||||
|
||||
if (bounce_type === 'hard') {
|
||||
// Permanent failure - remove from all lists
|
||||
await suppressEmail(email, 'hard_bounce');
|
||||
await removeFromAllLists(email);
|
||||
} else {
|
||||
// Soft bounce - track and remove after threshold
|
||||
await incrementSoftBounce(email);
|
||||
const count = await getSoftBounceCount(email);
|
||||
if (count >= 3) {
|
||||
await suppressEmail(email, 'soft_bounce_limit');
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Complaint Handling
|
||||
|
||||
```typescript
|
||||
async function handleComplaint(event) {
|
||||
const { email } = event.data;
|
||||
|
||||
// Immediate suppression - no exceptions
|
||||
await suppressEmail(email, 'complaint');
|
||||
await removeFromAllLists(email);
|
||||
await logComplaint(event); // For analysis
|
||||
}
|
||||
```
|
||||
|
||||
### Delivery Confirmation
|
||||
|
||||
```typescript
|
||||
async function handleDelivered(event) {
|
||||
const { email_id } = event.data;
|
||||
await updateEmailStatus(email_id, 'delivered');
|
||||
}
|
||||
```
|
||||
|
||||
## Idempotent Processing
|
||||
|
||||
Webhooks may be sent multiple times. Use event IDs to prevent duplicate processing.
|
||||
|
||||
```typescript
|
||||
async function processWebhook(event) {
|
||||
const eventId = event.id;
|
||||
|
||||
// Check if already processed
|
||||
if (await isEventProcessed(eventId)) {
|
||||
return; // Skip duplicate
|
||||
}
|
||||
|
||||
// Process event
|
||||
await handleEvent(event);
|
||||
|
||||
// Mark as processed
|
||||
await markEventProcessed(eventId);
|
||||
}
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
### Retry Behavior
|
||||
|
||||
If your endpoint returns non-2xx, webhooks will retry with exponential backoff:
|
||||
- Retry 1: ~30 seconds
|
||||
- Retry 2: ~1 minute
|
||||
- Retry 3: ~5 minutes
|
||||
- (continues for ~24 hours)
|
||||
|
||||
### Best Practices
|
||||
|
||||
- **Return 200 quickly** - Process asynchronously to avoid timeouts
|
||||
- **Be idempotent** - Handle duplicate deliveries gracefully
|
||||
- **Log everything** - Store raw events for debugging
|
||||
- **Alert on failures** - Monitor webhook processing errors
|
||||
- **Queue for processing** - Use a job queue for complex handling
|
||||
|
||||
## Testing Webhooks
|
||||
|
||||
**Local development:** Use ngrok or similar to expose localhost.
|
||||
|
||||
```bash
|
||||
ngrok http 3000
|
||||
# Use the ngrok URL as your webhook endpoint
|
||||
```
|
||||
|
||||
**Verify handling:** Send test events through Resend dashboard or manually trigger each event type.
|
||||
|
||||
## Related
|
||||
|
||||
- [List Management](./list-management.md) - What to do with bounce/complaint data
|
||||
- [Sending Reliability](./sending-reliability.md) - Retry logic when sends fail
|
||||
Reference in New Issue
Block a user