Skip to main content

Newsletter Webhooks

Handle newsletter events like bounces, unsubscribes, and engagement tracking in real-time.

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.

PropertyTypeDescription
newsletter.subscribedeventUser subscribed to newsletter
newsletter.unsubscribedeventUser unsubscribed
newsletter.confirmedeventUser confirmed double opt-in
newsletter.bouncedeventEmail bounced (soft or hard)
newsletter.complainedeventUser marked as spam
newsletter.openedeventCampaign email opened
newsletter.clickedeventLink 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,
  })
  break
Always 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 }
    )
  }
}