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