Quick Answer
Hard bounce webhook received — suppress the address immediately and permanently in your own database. Do not retry. NexusProMail enforces suppression at the API level automatically, but your application database must also reflect this to prevent re-importing the address in future list uploads. Hard bounce rate above 2% damages sender reputation. Automate suppression so it is never a manual step.
Hard vs soft bounce — what to suppress
Hard bounce (bounced.hard): Permanent failure — address does not exist, has been disabled, or server permanently rejected your sends. Suppress immediately and permanently.
Soft bounce (bounced.soft): Temporary failure — mailbox full, server temporarily unavailable. Do not suppress immediately. NexusProMail retries automatically. After 3+ consecutive soft bounces on the same address, treat as hard and suppress.
Two layers of suppression
- NexusProMail suppression list: Managed automatically. Hard bounces are added instantly. The API rejects sends to suppressed addresses with 422. No action needed on your part.
- Your application database: Your own records must reflect suppression status — otherwise the address can be re-imported in a future CSV upload, restarting the bounce cycle.
Python suppression handler
from datetime import datetime
def handle_bounce(event: dict, db):
email = event["data"]["email"]
if event["type"] == "bounced.hard":
suppress(db, email, "hard_bounce")
elif event["type"] == "bounced.soft":
count = increment_soft_bounce(db, email)
if count >= 3:
suppress(db, email, "repeated_soft_bounce")
elif event["type"] == "complained":
suppress(db, email, "complaint")
elif event["type"] == "unsubscribed":
suppress(db, email, "unsubscribed")
def suppress(db, email: str, reason: str):
db.execute("""
UPDATE contacts
SET email_status='suppressed', suppressed_at=%s, suppression_reason=%s
WHERE email=%s AND email_status!='suppressed'
""", (datetime.utcnow(), reason, email))
db.commit()Node.js suppression handler
async function handleBounce(event, db) {
const { email } = event.data
if (event.type === "bounced.hard") {
await suppress(db, email, "hard_bounce")
} else if (event.type === "bounced.soft") {
const count = await incrementSoftBounce(db, email)
if (count >= 3) await suppress(db, email, "repeated_soft_bounce")
} else if (event.type === "complained") {
await suppress(db, email, "complaint")
} else if (event.type === "unsubscribed") {
await suppress(db, email, "unsubscribed")
}
}
async function suppress(db, email, reason) {
await db.query(
"UPDATE contacts SET email_status=$1, suppressed_at=NOW(), suppression_reason=$2 WHERE email=$3 AND email_status!='suppressed'",
[reason, reason, email]
)
}Preventing re-import of suppressed addresses
async function importContacts(emails, db) {
const suppressed = await db.query(
"SELECT email FROM contacts WHERE email=ANY($1) AND email_status='suppressed'",
[emails]
)
const suppressedSet = new Set(suppressed.rows.map(r => r.email))
const toImport = emails.filter(e => !suppressedSet.has(e))
console.log("Skipping suppressed:", emails.length - toImport.length)
return toImport
}Suppression vs deletion
Keep suppressed records — do not delete them. The suppression check only works if the record exists. When a contact requests GDPR erasure, NexusProMail DSAR tooling handles full deletion including erasure tombstones that prevent re-import even after the record is gone.
For full deliverability impact of bounce rates, see the email bounce rate guide. For GDPR erasure context, see the GDPR email marketing guide. For the webhook setup that feeds this handler, see email webhooks guide.