Outbound with Engagement Awareness
A one-way blast is not a conversation. mailbot tracks delivery, opens, clicks, and bounces on every outbound message and pushes those events to your webhook. This guide shows how to send a message, listen for engagement, and react in your code.
The pattern lets an agent score leads, adapt follow-ups, and stop sending to people who never engage.
What gets tracked
For every outbound message, mailbot emits events as they happen:
| Event | Trigger |
|---|---|
message.sent | Upstream MTA accepted the message |
message.delivered | Recipient mailbox accepted delivery |
message.opened | Tracking pixel loaded (HTML body only) |
message.clicked | Tracking link followed |
message.bounced | Mailbox refused delivery |
Open tracking uses an invisible 1×1 pixel; click tracking rewrites links. Both are opt-in per message via the track_opens and track_clicks flags on send.
Step 1: Send with tracking on
import { MailBot } from '@yopiesuryadi/mailbot-sdk';
const mailbot = new MailBot({ apiKey: process.env.MAILBOT_API_KEY });
const message = await mailbot.messages.send({
inbox_id: 'inb_xxx',
to: ['lead@prospect.com'],
subject: 'Following up on our conversation',
body_html: `
<p>Hi,</p>
<p>Want to dig in further? Here is the doc:
<a href="https://yourapp.example.com/onepager">View one-pager</a>.</p>
`,
body_text: 'Want to dig in further? View one-pager: https://yourapp.example.com/onepager',
track_opens: true,
track_clicks: true,
labels: ['outbound', 'campaign:march-followup'],
});
Always include body_text alongside body_html. Recipients on plain-text clients (or when HTML is blocked) still see the message.
Step 2: Subscribe to events
Register a webhook for the engagement events you care about.
curl -X POST https://getmail.bot/v1/inboxes/$INBOX_ID/webhooks \
-H "Authorization: Bearer $MAILBOT_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://yourapp.example.com/webhooks/engagement",
"events": ["message.opened", "message.clicked", "message.bounced"]
}'
Step 3: React in your code
Your webhook handler updates lead state when engagement happens.
app.post('/webhooks/engagement', async (req, res) => {
// verify signature first (omitted for brevity, see Webhook reliability)
const event = req.body;
const lead = await db.leads.findByMessageId(event.message_id);
if (!lead) return res.status(204).end();
switch (event.event_type) {
case 'message.opened':
await db.leads.update(lead.id, {
last_open_at: event.created_at,
open_count: { increment: 1 },
score: { increment: 5 },
});
break;
case 'message.clicked':
await db.leads.update(lead.id, {
last_click_at: event.created_at,
click_count: { increment: 1 },
score: { increment: 15 },
clicked_url: event.url,
});
await maybeTriggerHotLeadFlow(lead.id);
break;
case 'message.bounced':
await db.leads.update(lead.id, {
status: 'bounced',
suppressed: true,
});
break;
}
res.status(200).end();
});
A click is a much stronger signal than an open. The score weights here (+5 for open, +15 for click) are an example. Pick weights that match your conversion data.
Step 4: Adapt follow-up
Run a worker that decides who to email next based on engagement state.
async function pickFollowUpCohort() {
return db.leads.find({
status: 'active',
suppressed: false,
score: { gte: 20 },
last_open_at: { gte: new Date(Date.now() - 7 * 86400_000) },
});
}
Then send the next-step message to that cohort. People who have not opened anything in 7 days get a different (or no) follow-up.
Lead scoring example
A simple scoring rubric using mailbot's events:
| Signal | Score |
|---|---|
| Email delivered | 0 |
| Opened once | +5 |
| Opened 3+ times | +10 (cumulative) |
| Clicked link | +15 |
| Replied | +30 |
| Bounced | suppress |
Apply on each event. Reset to 0 if 30 days pass with no signal.
Practical tips
- Suppress on bounce. A bounce means the address is invalid (hard) or temporarily unreachable (soft). Stop sending to hard bounces immediately. Soft bounces deserve a retry, not a permanent ban.
- Honor unsubscribe. If your message includes an unsubscribe link, treat the click as
suppress, not as engagement. - Cap open weight. Some clients pre-fetch images to scan for malware, which fires opens without human intent. Weight opens lower than clicks.
- Trust replies most. A reply event has the highest signal-to-noise. If you can reach a reply state, you do not need to keep guessing from opens.
- Use labels for cohorts. Pass
labels: ['campaign:xyz']on send. Filtering events by label downstream is then trivial.