Available Events
Newsletter webhooks notify your application when important events occur. Open and click tracking require tracking pixels to be enabled in your campaign settings.
| Property | Type | Description |
|---|---|---|
newsletter.subscribed | event | User subscribed to newsletter |
newsletter.unsubscribed | event | User unsubscribed |
newsletter.confirmed | event | User confirmed double opt-in |
newsletter.bounced | event | Email bounced (soft or hard) |
newsletter.complained | event | User marked as spam |
newsletter.opened | event | Campaign email opened |
newsletter.clicked | event | Link clicked in campaign |
Setting Up Webhooks
Configure newsletter webhooks in your Console under Settings > Webhooks or programmatically:
setup-webhook.ts
import { sylphx } from '@sylphx/sdk'
// Create a webhook endpoint for newsletter events
await sylphx.webhooks.create({
url: 'https://your-app.com/api/webhooks/newsletter',
events: [
'newsletter.subscribed',
'newsletter.unsubscribed',
'newsletter.bounced',
'newsletter.opened',
'newsletter.clicked',
],
})Event Payloads
Each event includes relevant data about the subscriber and action:
webhook-handler.ts
// api/webhooks/newsletter/route.ts
import { NextRequest, NextResponse } from 'next/server'
import { sylphx } from '@sylphx/sdk/server'
export async function POST(req: NextRequest) {
// Verify webhook signature
const payload = await sylphx.webhooks.verify(req)
switch (payload.event) {
case 'newsletter.subscribed':
// payload.data: { email, preferences, subscribedAt }
console.log('New subscriber:', payload.data.email)
break
case 'newsletter.unsubscribed':
// payload.data: { email, reason, unsubscribedAt }
await removeFromMailingList(payload.data.email)
break
case 'newsletter.bounced':
// payload.data: { email, bounceType, bouncedAt }
if (payload.data.bounceType === 'hard') {
await markEmailInvalid(payload.data.email)
}
break
case 'newsletter.opened':
// payload.data: { email, campaignId, openedAt, userAgent }
await trackEngagement(payload.data)
break
case 'newsletter.clicked':
// payload.data: { email, campaignId, url, clickedAt }
await trackClick(payload.data)
break
}
return NextResponse.json({ received: true })
}Handling Bounces
Proper bounce handling is critical for maintaining sender reputation:
Hard Bounces
Permanent delivery failures:
- • Invalid email address
- • Domain doesn't exist
- • Recipient rejected
Soft Bounces
Temporary delivery failures:
- • Mailbox full
- • Server temporarily unavailable
- • Message too large
Best practice: Remove hard bounces immediately, retry soft bounces 3 times before removing.
bounce-handler.ts
async function handleBounce(data: {
email: string
bounceType: 'hard' | 'soft'
bounceCount?: number
}) {
if (data.bounceType === 'hard') {
// Immediately unsubscribe hard bounces
await sylphx.newsletter.unsubscribe(data.email, {
reason: 'hard_bounce',
suppressFuture: true,
})
} else {
// Track soft bounces, unsubscribe after 3
const bounceCount = (data.bounceCount ?? 0) + 1
if (bounceCount >= 3) {
await sylphx.newsletter.unsubscribe(data.email, {
reason: 'soft_bounce_limit',
})
} else {
await updateBounceCount(data.email, bounceCount)
}
}
}Spam Complaints
Handle spam complaints to maintain your sender reputation and comply with regulations:
complaint-handler.ts
case 'newsletter.complained':
// Immediately remove and suppress
await sylphx.newsletter.unsubscribe(payload.data.email, {
reason: 'spam_complaint',
suppressFuture: true, // Never send to this email again
})
// Log for compliance records
await logComplaint({
email: payload.data.email,
campaignId: payload.data.campaignId,
complainedAt: payload.data.complainedAt,
})
breakAlways suppress future emails to spam complainants. Continuing to send can result in blacklisting.
Engagement Analytics
Track opens and clicks to measure campaign effectiveness:
engagement-tracking.ts
import { sylphx } from '@sylphx/sdk'
// Get campaign engagement stats
const stats = await sylphx.newsletter.getCampaignStats(campaignId)
console.log({
sent: stats.sent,
delivered: stats.delivered,
opened: stats.opened,
clicked: stats.clicked,
bounced: stats.bounced,
unsubscribed: stats.unsubscribed,
// Calculated rates
openRate: (stats.opened / stats.delivered * 100).toFixed(2) + '%',
clickRate: (stats.clicked / stats.opened * 100).toFixed(2) + '%',
bounceRate: (stats.bounced / stats.sent * 100).toFixed(2) + '%',
})Webhook Security
Always verify webhook signatures to ensure requests are from Sylphx:
verify-webhook.ts
import { sylphx } from '@sylphx/sdk/server'
import { headers } from 'next/headers'
export async function POST(req: NextRequest) {
const headersList = await headers()
const signature = headersList.get('x-sylphx-signature')
if (!signature) {
return NextResponse.json(
{ error: 'Missing signature' },
{ status: 401 }
)
}
try {
// verify() throws if signature is invalid
const payload = await sylphx.webhooks.verify(req)
// Process verified payload...
} catch (error) {
return NextResponse.json(
{ error: 'Invalid signature' },
{ status: 401 }
)
}
}