Misar IO Docs
MisarMailApi Reference

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

MethodPathDescription
POST/api/list-health/runTrigger a full list health check
GET/api/list-health/statsGet current health stats
POST/api/list-health/win-backSchedule a win-back campaign
POST/api/contacts/health/runTrigger contacts-scoped health check
GET/api/contacts/health/statsGet contacts health stats
GET/api/contacts/health/unengagedList unengaged contacts
GET / POST/api/contacts/scoring-rulesGet 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.

FieldTypeRequiredDescription
campaignIdstringYesID of the campaign to send
thresholdDaysintegerNoDays 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

ParameterTypeDefaultDescription
thresholdDaysinteger90Days of inactivity to qualify as unengaged
pageinteger1Page number
limitinteger20Results 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.

FieldTypeDescription
eventstringEvent type: open, click, reply, bounce, complaint
pointsintegerPoints awarded (negative values subtract)
decay_daysintegerDays 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

  1. Run /api/list-health/run to refresh stats.
  2. Check /api/list-health/stats — note unengagedCount and healthScore.
  3. Create a re-engagement campaign in the dashboard (subject line: "We miss you — here's 20% off").
  4. Call /api/list-health/win-back with the campaign ID and your chosen thresholdDays.
  5. 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.