Beyond bounces, spam complaints and unsubscribes are critical signals that affect your sender reputation. Handling these events properly is essential for maintaining deliverability and legal compliance.
Spam Complaints
A spam complaint occurs when a recipient clicks “Report Spam” or “Mark as Junk” in their email client. This is one of the most damaging signals for your sender reputation.
Understanding Complaints
When a recipient complains, their email provider sends a feedback report to Lettr. The address is automatically suppressed and you receive a message.spam_complaint webhook event.
[{
"msys" : {
"message_event" : {
"type" : "spam_complaint" ,
"timestamp" : "1705320000" ,
"transmission_id" : "12345678" ,
"message_id" : "abcd-1234-efgh" ,
"rcpt_to" : "recipient@example.com" ,
"fbtype" : "abuse" ,
"friendly_from" : "you@example.com" ,
"subject" : "Your Weekly Newsletter" ,
"sending_ip" : "192.168.1.1" ,
"rcpt_meta" : {
"userId" : "user_789"
}
}
}
}]
Field Type Description message_idstring Unique identifier for the email rcpt_tostring Recipient who complained timestampstring Unix timestamp of the complaint fbtypestring Type of feedback (typically abuse) rcpt_metaobject Custom metadata from the original send
Complaint Rate Guidelines
Complaint Rate Status Action < 0.1% Healthy Maintain current practices 0.1-0.3% Warning Review targeting and content > 0.3% Critical Pause sending and investigate
A complaint rate above 0.1% is considered high by most email providers. Rates above 0.3% can result in your emails being blocked entirely.
Handling Complaints
app . post ( '/webhooks/lettr' , express . json (), async ( req , res ) => {
res . sendStatus ( 200 );
for ( const event of req . body ) {
const eventType = Object . keys ( event . msys )[ 0 ];
const data = event . msys [ eventType ];
if ( data . type === 'spam_complaint' ) {
// Immediately suppress in your system
await db . subscribers . update ({
where: { email: data . rcpt_to },
data: {
status: 'complained' ,
canEmail: false ,
complainedAt: new Date ( parseInt ( data . timestamp ) * 1000 )
}
});
// Log for analysis
await logComplaint ({
email: data . rcpt_to ,
messageId: data . message_id ,
userId: data . rcpt_meta ?. userId ,
timestamp: new Date ( parseInt ( data . timestamp ) * 1000 )
});
// Alert team - complaints are serious
await alertTeam ({
type: 'spam_complaint' ,
email: data . rcpt_to ,
messageId: data . message_id ,
severity: 'high'
});
}
}
});
Reducing Complaints
Never email people who haven’t explicitly opted in to receive your emails. Purchased lists have extremely high complaint rates.
Tell subscribers what kind of emails they’ll receive and how often. Unexpected emails lead to complaints.
A visible unsubscribe link gives recipients an alternative to marking you as spam. Don’t hide it.
Honor Frequency Preferences
If someone signs up for weekly updates, don’t email them daily. Respect the frequency they agreed to.
Irrelevant emails frustrate recipients. Segment your list and send targeted content.
Unsubscribes
Unsubscribes occur when recipients opt out of receiving your emails. Lettr supports two unsubscribe methods, each with its own event type.
List-Unsubscribe
Modern email clients show an “Unsubscribe” button in their interface. When clicked, this triggers a List-Unsubscribe request directly to Lettr, which generates an unsubscribe.list_unsubscribe event.
[{
"msys" : {
"unsubscribe_event" : {
"type" : "list_unsubscribe" ,
"timestamp" : "1705320000" ,
"transmission_id" : "12345678" ,
"message_id" : "abcd-1234-efgh" ,
"rcpt_to" : "recipient@example.com" ,
"friendly_from" : "you@example.com" ,
"subject" : "Your Weekly Newsletter" ,
"rcpt_meta" : {
"userId" : "user_789"
}
}
}
}]
Link Unsubscribe
When a recipient clicks an unsubscribe link in your email body, this triggers an unsubscribe.link_unsubscribe event.
[{
"msys" : {
"unsubscribe_event" : {
"type" : "link_unsubscribe" ,
"timestamp" : "1705320000" ,
"transmission_id" : "12345678" ,
"message_id" : "abcd-1234-efgh" ,
"rcpt_to" : "recipient@example.com" ,
"friendly_from" : "you@example.com" ,
"subject" : "Your Weekly Newsletter" ,
"rcpt_meta" : {
"userId" : "user_789"
}
}
}
}]
Field Type Description message_idstring Unique identifier for the email rcpt_tostring Recipient who unsubscribed timestampstring Unix timestamp of the unsubscribe typestring list_unsubscribe or link_unsubscribercpt_metaobject Custom metadata from the original send
Handling Unsubscribes
app . post ( '/webhooks/lettr' , express . json (), async ( req , res ) => {
res . sendStatus ( 200 );
for ( const event of req . body ) {
const eventType = Object . keys ( event . msys )[ 0 ];
const data = event . msys [ eventType ];
if ( data . type === 'list_unsubscribe' || data . type === 'link_unsubscribe' ) {
// Update subscription status
await db . subscribers . update ({
where: { email: data . rcpt_to },
data: {
status: 'unsubscribed' ,
canEmail: false ,
unsubscribedAt: new Date ( parseInt ( data . timestamp ) * 1000 ),
unsubscribeMethod: data . type
}
});
// Log for analytics
await analytics . track ( 'unsubscribe' , {
email: data . rcpt_to ,
method: data . type ,
userId: data . rcpt_meta ?. userId
});
}
}
});
Legal Requirements
Unsubscribe requests must be honored. Both CAN-SPAM (US) and GDPR (EU) require you to stop emailing someone who has unsubscribed. Violations can result in significant fines.
CAN-SPAM Requirements:
Process unsubscribe requests within 10 business days
Don’t charge a fee or require personal information to unsubscribe
Don’t transfer or sell the email address after unsubscribe
GDPR Requirements:
Process unsubscribe requests without delay
Provide clear and accessible unsubscribe mechanisms
Maintain records of consent and withdrawal
Managing Your Suppression Data
While Lettr automatically suppresses addresses that bounce, complain, or unsubscribe, you should also maintain your own records for several reasons:
Why Maintain Your Own Records
Prevent re-adding suppressed addresses : When someone unsubscribes or complains, you shouldn’t re-add them to your list
Cross-platform consistency : If you use multiple email providers, share suppression data between them
Compliance auditing : Maintain proof that you honored unsubscribe requests
List hygiene : Track patterns to improve your email program
Building a Suppression System
// Example suppression record schema
const suppressionSchema = {
email: String , // The suppressed email address
type: String , // 'bounce', 'complaint', or 'unsubscribe'
reason: String , // Detailed reason (e.g., bounce_class or method)
source: String , // Where suppression originated
messageId: String , // Related message ID if applicable
createdAt: Date , // When suppression was added
metadata: Object // Additional context
};
// Check before sending
async function canSendTo ( email ) {
const suppression = await db . suppressions . findOne ({ email });
return ! suppression ;
}
// Add suppression from webhook
async function addSuppression ( data ) {
let type , reason ;
switch ( data . type ) {
case 'bounce' :
const bounceClass = parseInt ( data . bounce_class );
if ( ! [ 10 , 30 , 100 ]. includes ( bounceClass )) return ; // Only suppress hard bounces
type = 'bounce' ;
reason = `bounce_class_ ${ data . bounce_class } ` ;
break ;
case 'spam_complaint' :
type = 'complaint' ;
reason = data . fbtype || 'abuse' ;
break ;
case 'list_unsubscribe' :
case 'link_unsubscribe' :
type = 'unsubscribe' ;
reason = data . type ;
break ;
default :
return ;
}
await db . suppressions . upsert ({
where: { email: data . rcpt_to },
create: {
email: data . rcpt_to ,
type ,
reason ,
source: 'lettr_webhook' ,
messageId: data . message_id ,
createdAt: new Date (),
metadata: data . rcpt_meta
},
update: {
type ,
reason ,
updatedAt: new Date ()
}
});
}
Pre-Send Validation
Before sending emails, validate addresses against your suppression list:
async function sendEmail ( to , subject , html ) {
// Check suppression status
const canSend = await canSendTo ( to );
if ( ! canSend ) {
console . log ( `Skipping suppressed address: ${ to } ` );
return { status: 'suppressed' };
}
// Proceed with sending
const response = await fetch ( 'https://app.lettr.com/api/emails' , {
method: 'POST' ,
headers: {
'Authorization' : `Bearer ${ API_KEY } ` ,
'Content-Type' : 'application/json'
},
body: JSON . stringify ({
from: 'you@example.com' ,
to: [ to ],
subject ,
html
})
});
return response . json ();
}
Webhook Event Summary
Subscribe to these events to track all suppression-related activity:
Event Type Trigger Suppression message.bounce (hard)Permanent delivery failure Automatic message.spam_complaintSpam complaint Automatic unsubscribe.list_unsubscribeList-Unsubscribe header Automatic unsubscribe.link_unsubscribeUnsubscribe link click Automatic
Configure your webhook to receive these events:
// When creating a webhook, include these event types
const webhookEvents = [
'message.bounce' ,
'message.spam_complaint' ,
'unsubscribe.list_unsubscribe' ,
'unsubscribe.link_unsubscribe'
];