email skill
This commit is contained in:
@@ -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