List Health
Monitor your mailing list quality, detect unengaged subscribers, score contacts, and run win-back campaigns
List health tools help you maintain a clean, engaged mailing list. A healthy list improves deliverability, lowers spam complaint rates, and ensures your campaigns reach people who actually want to hear from you.
Authentication
All list health endpoints use session (cookie-based) authentication. They are dashboard-facing and do not accept API keys.
Health score
The healthScore is a 0–100 integer computed from your contact base:
- 100 — all contacts are active and engaged
- 0 — all contacts are suppressed, bounced, or unengaged
Score is weighted across four signals: active ratio, bounce rate, complaint rate, and recent engagement. Run a health check to update it.
Unengaged contacts
A contact is considered unengaged when they have not opened any email within the configured threshold (default: 90 days). Unengaged contacts lower your health score and are the primary target for win-back campaigns.
Endpoints
| Method | Path | Description |
|---|---|---|
POST | /api/list-health/run | Trigger a full list health check |
GET | /api/list-health/stats | Get current health stats |
POST | /api/list-health/win-back | Schedule a win-back campaign |
POST | /api/contacts/health/run | Trigger contacts-scoped health check |
GET | /api/contacts/health/stats | Get contacts health stats |
GET | /api/contacts/health/unengaged | List unengaged contacts |
GET / POST | /api/contacts/scoring-rules | Get or update engagement scoring rules |
Run a list health check
POST /api/list-health/run
Triggers a full health analysis. The job runs asynchronously — poll /api/list-health/stats until lastRunAt updates.
curl -X POST https://api.misar.io/mail/api/list-health/run \
-H "Cookie: session=..."const res = await fetch("https://api.misar.io/mail/api/list-health/run", {
method: "POST",
credentials: "include",
});
const { data } = await res.json();Response
{
"success": true,
"data": {
"jobId": "job_01hvxyz",
"status": "queued"
}
}Get health stats
GET /api/list-health/stats
curl https://api.misar.io/mail/api/list-health/stats \
-H "Cookie: session=..."const res = await fetch("https://api.misar.io/mail/api/list-health/stats", {
credentials: "include",
});
const { data } = await res.json();Response
{
"success": true,
"data": {
"totalContacts": 12400,
"activeContacts": 10850,
"unengagedCount": 980,
"bouncedCount": 320,
"complainedCount": 44,
"healthScore": 87,
"lastRunAt": "2026-05-27T06:00:00Z"
}
}Schedule a win-back campaign
POST /api/list-health/win-back
Targets contacts who have not opened an email in the last thresholdDays days and schedules the specified campaign to send to them.
| Field | Type | Required | Description |
|---|---|---|---|
campaignId | string | Yes | ID of the campaign to send |
thresholdDays | integer | No | Days of inactivity to qualify. Default 90. |
curl -X POST https://api.misar.io/mail/api/list-health/win-back \
-H "Cookie: session=..." \
-H "Content-Type: application/json" \
-d '{ "campaignId": "cam_01hvabc", "thresholdDays": 60 }'const res = await fetch("https://api.misar.io/mail/api/list-health/win-back", {
method: "POST",
credentials: "include",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ campaignId: "cam_01hvabc", thresholdDays: 60 }),
});
const { data } = await res.json();Response
{
"success": true,
"data": {
"campaignId": "cam_01hvabc",
"targetedContacts": 980,
"thresholdDays": 60,
"scheduledAt": "2026-05-27T10:05:00Z"
}
}Contacts health check (scoped)
POST /api/contacts/health/run
Same as the full list health check but scoped to contact-level signals only (bounces, complaints, engagement). Faster for large lists when you only need contact data refreshed.
Response mirrors /api/list-health/run — returns { jobId, status: "queued" }.
Contacts health stats (scoped)
GET /api/contacts/health/stats
Returns a contacts-level subset of the full health stats — same response shape as /api/list-health/stats without campaign-level signals.
List unengaged contacts
GET /api/contacts/health/unengaged
| Parameter | Type | Default | Description |
|---|---|---|---|
thresholdDays | integer | 90 | Days of inactivity to qualify as unengaged |
page | integer | 1 | Page number |
limit | integer | 20 | Results per page (max 100) |
curl "https://api.misar.io/mail/api/contacts/health/unengaged?thresholdDays=60&limit=50" \
-H "Cookie: session=..."const res = await fetch(
"https://api.misar.io/mail/api/contacts/health/unengaged?thresholdDays=60&limit=50",
{ credentials: "include" }
);
const { data } = await res.json();Response
{
"success": true,
"data": {
"contacts": [
{
"id": "con_01hvghi",
"email": "[email protected]",
"status": "subscribed",
"last_opened_at": "2026-01-10T09:22:00Z",
"days_inactive": 137,
"engagement_score": 12
}
],
"pagination": {
"page": 1,
"limit": 50,
"total": 980,
"total_pages": 20
}
}
}Contact engagement scoring rules
GET /api/contacts/scoring-rules
Returns the current ruleset that defines how engagement points are awarded.
POST /api/contacts/scoring-rules
Replaces the scoring ruleset. Send an array of rule objects.
| Field | Type | Description |
|---|---|---|
event | string | Event type: open, click, reply, bounce, complaint |
points | integer | Points awarded (negative values subtract) |
decay_days | integer | Days after which this rule's points expire |
curl https://api.misar.io/mail/api/contacts/scoring-rules \
-H "Cookie: session=..."curl -X POST https://api.misar.io/mail/api/contacts/scoring-rules \
-H "Cookie: session=..." \
-H "Content-Type: application/json" \
-d '[
{ "event": "open", "points": 5, "decay_days": 90 },
{ "event": "click", "points": 10, "decay_days": 90 },
{ "event": "reply", "points": 20, "decay_days": 180 },
{ "event": "bounce", "points": -20, "decay_days": 0 },
{ "event": "complaint", "points": -50, "decay_days": 0 }
]'GET response
{
"success": true,
"data": [
{ "event": "open", "points": 5, "decay_days": 90 },
{ "event": "click", "points": 10, "decay_days": 90 },
{ "event": "reply", "points": 20, "decay_days": 180 },
{ "event": "bounce", "points": -20, "decay_days": 0 },
{ "event": "complaint", "points": -50, "decay_days": 0 }
]
}Recommended win-back flow
- Run
/api/list-health/runto refresh stats. - Check
/api/list-health/stats— noteunengagedCountandhealthScore. - Create a re-engagement campaign in the dashboard (subject line: "We miss you — here's 20% off").
- Call
/api/list-health/win-backwith the campaign ID and your chosenthresholdDays. - After the campaign sends, run another health check. Contacts who open or click will have their engagement score updated and will no longer appear as unengaged.
Contacts who do not respond to a win-back campaign are candidates for suppression. Continuing to send to chronically unengaged addresses harms your sender reputation. Use the unengaged list to unsubscribe or suppress non-responders after the win-back window closes.