← Back to Blog
Developer Guides13 May 2026 · NexusProMail Team

Email Webhooks: How to Handle Bounces, Complaints and Delivery Events

Webhooks deliver real-time email events — bounces, complaints, opens, clicks, unsubscribes — to your application. This guide covers event types, payload structure, signature verification and the correct handling pattern for each event.

An email leaves your application and you receive an API response confirming it was accepted for delivery. But what happens after that? Did it reach the inbox? Did it bounce? Did the recipient mark it as spam? Without webhooks, you have no way to know. Webhooks push delivery event data to your application in real time — and handling them correctly is essential for maintaining list quality and sender reputation.

What Are Email Webhooks?

A webhook is an HTTP callback. When a delivery event occurs — a bounce, a click, an unsubscribe — NexusProMail makes an HTTP POST request to your registered endpoint with a JSON payload describing the event. Your application receives the data, processes it and responds with a 200 status code to acknowledge receipt.

Webhooks replace the need to poll the API for delivery status. For high-volume sending, polling is impractical. Webhooks push the data as events occur.

Registering a Webhook Endpoint

Register your webhook endpoint via the API:

curl -X POST https://app.nexuspromail.com/api/v1/api/webhooks \
  -H 'Authorization: Bearer YOUR_API_KEY' \
  -H 'Content-Type: application/json' \
  -d '{
    "url": "https://yourapp.com/webhooks/email",
    "events": ["bounced.hard", "bounced.soft", "complained", "unsubscribed", "delivered", "opened", "clicked"]
  }'

The response includes your webhook secret. Store it as an environment variable (WEBHOOK_SECRET) — you will use it to verify incoming payloads.

Webhook Payload Structure

Every webhook event follows a consistent structure:

{
  "id": "evt_01hx...",
  "type": "bounced.hard",
  "created_at": "2026-05-13T10:24:31Z",
  "data": {
    "message_id": "msg_01hx...",
    "email": "recipient@example.com",
    "timestamp": "2026-05-13T10:24:28Z",
    "bounce_code": "550",
    "bounce_reason": "5.1.1 The email account that you tried to reach does not exist"
  }
}

Key fields: type identifies the event; data.message_id correlates to the original send; data.email is the affected recipient address.

Verifying Webhook Signatures

Every webhook request includes an X-NexusProMail-Signature header containing an HMAC-SHA256 signature of the raw request body, computed using your webhook secret. Always verify this before processing the payload. An unverified webhook could be forged.

const crypto = require('crypto')

function verifyWebhookSignature(rawBody, receivedSignature, secret) {
  const computed = crypto
    .createHmac('sha256', secret)
    .update(rawBody, 'utf8')
    .digest('hex')

  // Use timingSafeEqual to prevent timing-based attacks
  if (computed.length !== receivedSignature.length) return false
  return crypto.timingSafeEqual(
    Buffer.from(computed, 'hex'),
    Buffer.from(receivedSignature, 'hex')
  )
}

Important: read the raw request body for signature computation. Do not use the parsed JSON — any whitespace difference between the raw body and re-serialised JSON will break signature verification.

How to Handle Each Event Type

bounced.hard

A hard bounce means the address does not exist or has permanently rejected the message. Action: suppress the address immediately in your own database. Do not attempt to resend. Continuing to send to hard-bounced addresses will damage your sender reputation and will be rejected by NexusProMail's suppression enforcement anyway.

case 'bounced.hard':
  await db.execute(
    'UPDATE users SET email_status = $1, email_bounced_at = NOW() WHERE email = $2',
    ['hard_bounced', event.data.email]
  )
  // Also remove from any active marketing lists
  await db.execute(
    'DELETE FROM mailing_list_members WHERE email = $1',
    [event.data.email]
  )
  break

bounced.soft

A soft bounce is a temporary failure. NexusProMail retries automatically. Your webhook receives a soft bounce notification but you generally do not need to act immediately. Track consecutive soft bounces per address — if the same address generates 3+ soft bounces without a successful delivery, treat it as a potential hard bounce and investigate.

complained

A spam complaint is the most damaging deliverability event. Suppress the address immediately and do not contact them again for any marketing purpose. Even transactional email to a complainant should be reviewed — if the person considered your email spam, continued contact may generate more complaints.

unsubscribed

The recipient clicked the unsubscribe link. Update their subscription status in your database. Do not send further marketing email. NexusProMail enforces suppression automatically, but keeping your own database current prevents incorrect status in your CRM or customer records.

delivered, opened, clicked

Positive engagement events. Use these to update last-active timestamps, trigger follow-up sequences and feed engagement scoring for segmentation. For segmentation purposes, opened and clicked are more valuable signals than delivered.

Webhook Reliability Patterns

Respond with 200 immediately

Return a 200 status code as quickly as possible — before you process the event. Use a queue to handle processing asynchronously. If your webhook endpoint is slow or returns a non-200 response, NexusProMail will retry delivery, which can cause duplicate event processing.

app.post('/webhooks/email', express.raw({ type: 'application/json' }), async (req, res) => {
  // Verify signature first
  const sig = req.headers['x-nexuspromail-signature']
  if (!verifyWebhookSignature(req.body, sig, process.env.WEBHOOK_SECRET)) {
    return res.status(401).json({ error: 'Invalid signature' })
  }

  // Acknowledge immediately
  res.sendStatus(200)

  // Process asynchronously
  const event = JSON.parse(req.body)
  await eventQueue.push(event) // or setImmediate(() => processEvent(event))
})

Handle duplicates idempotently

Webhook events can be delivered more than once. Use event.id as an idempotency key — check if you have already processed this event ID before acting on it. Store processed event IDs in a cache or database with a TTL of 24–48 hours.

Log every event

Log the full event payload before processing. If your processing logic has a bug, the log gives you the raw data to replay events once the bug is fixed.

Further reading

Related reading

Email deliverability guideGDPR complianceTransactional email API

Start sending with NexusProMail

Launch email campaigns and transactional emails from one platform.

Start FreeView Pricing

Free plan · No credit card required · GDPR-compliant · Built in Finland

Email Webhooks: How to Handle Bounces, Complaints and Delivery Events | NexusProMail