List Health
Monitor your mailing list quality, detect unengaged subscribers, score contacts, and run win-back workflows
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.
All list-health endpoints use session (cookie-based) authentication. They are dashboard-facing and do not accept API keys.
Sunset stages
List health works by identifying unengaged contacts — subscribers who have not opened or clicked within the configured window — and moving them through progressive sunset stages: warned → send frequency reduced → removed (suppressed). Each health run scans your contacts and advances or records these stages.
Run a list health check
/api/list-health/runRuns a full health scan synchronously and returns the run's stats. Pass dryRun: true to compute what would change without writing anything.
Request body
dryRunbooleanbodydefault: falseWhen true, the scan reports counts without warning, reducing, or removing any contacts.
Response fields
successbooleantrue when the run completed.
runIdstringID of the recorded run.
dryRunbooleanEchoes the requested mode.
statsobjectscanned, unengaged, warned, and removed counts for this run.
curl -X POST /api/list-health/run \
-H "Content-Type: application/json" \
-d '{ "dryRun": false }'{
"success": true,
"runId": "9f1c2a7e-4b8d-4c2a-9e11-2f3a4b5c6d7e",
"dryRun": false,
"stats": {
"scanned": 12400,
"unengaged": 980,
"warned": 640,
"removed": 120
}
}Get health stats
/api/list-health/statsReturns current list-health counters, the state of the win-back workflow, and recent runs.
Response fields
currentStatsobjecttotalSubscribed, unengaged, sunsetWarned, sunsetReduced, and sunsetRemoved.
winBackWorkflowobjectexists, workflowId, status, totalEnrolled, and totalCompleted.
recentRunsArray<object>Recent runs, each with id, run_type, started_at, completed_at, and a stats object (scanned, unengaged, warned, removed).
curl /api/list-health/stats{
"currentStats": {
"totalSubscribed": 12400,
"unengaged": 980,
"sunsetWarned": 640,
"sunsetReduced": 210,
"sunsetRemoved": 120
},
"winBackWorkflow": {
"exists": true,
"workflowId": "7c1b2a3d-...",
"status": "active",
"totalEnrolled": 640,
"totalCompleted": 180
},
"recentRuns": [
{
"id": "9f1c2a7e-...",
"run_type": "list_health",
"started_at": "2026-05-27T06:00:00Z",
"completed_at": "2026-05-27T06:00:12Z",
"stats": { "scanned": 12400, "unengaged": 980, "warned": 640, "removed": 120 }
}
]
}Enroll contacts in the win-back workflow
/api/list-health/win-backEnrolls the given contacts into the account's win-back workflow, creating the workflow if it does not already exist. Contacts already enrolled are skipped.
Request body
contactIdsUUID[]bodyrequiredContact IDs to enroll (1–1000).
Response fields
successbooleantrue when enrollment completed.
dataobjectworkflowId, workflowCreated (whether the workflow was created by this call), enrolled (newly enrolled), and alreadyEnrolled.
curl -X POST /api/list-health/win-back \
-H "Content-Type: application/json" \
-d '{ "contactIds": ["con_...","con_..."] }'{
"success": true,
"data": {
"workflowId": "7c1b2a3d-...",
"workflowCreated": false,
"enrolled": 2,
"alreadyEnrolled": 0
}
}Contacts health check (scoped)
/api/contacts/health/runRuns the contact-level health scan synchronously (sunset detection). Pass dryRun to preview.
Request body
dryRunbooleanbodydefault: falseWhen true, reports counts without mutating contacts.
Response fields
successbooleantrue when the run completed. The response spreads the run stats alongside success: scanned, unengaged_detected, sunset_warned, sunset_reduced, and sunset_removed.
curl -X POST /api/contacts/health/run \
-H "Content-Type: application/json" \
-d '{ "dryRun": false }'{
"success": true,
"scanned": 12400,
"unengaged_detected": 980,
"sunset_warned": 640,
"sunset_reduced": 210,
"sunset_removed": 120
}Contacts health stats (scoped)
/api/contacts/health/statsReturns contact-level sunset counters.
Response fields
unengaged_totalintegerTotal contacts currently flagged unengaged.
sunset_warnedintegerContacts in the warned stage.
sunset_reducedintegerContacts in the reduced stage.
sunset_removedintegerContacts that have been removed/suppressed.
duplicatesintegerDetected duplicate contacts.
win_back_activeintegerContacts currently enrolled in the win-back workflow.
last_run_atstring | nullISO-8601 time of the most recent contacts-health run.
curl /api/contacts/health/stats{
"unengaged_total": 980,
"sunset_warned": 640,
"sunset_reduced": 210,
"sunset_removed": 120,
"duplicates": 34,
"win_back_active": 460,
"last_run_at": "2026-05-27T06:00:00Z"
}List unengaged contacts
/api/contacts/health/unengagedReturns unengaged contacts as a plain array (no wrapper), using offset-based paging.
Query parameters
limitintegerquerydefault: 25Results to return (max 100).
offsetintegerquerydefault: 0Number of results to skip.
Response
Returns a JSON array of contacts, each with id, email, last_engagement_at, sunset_stage, and engagement_tier.
curl "/api/contacts/health/unengaged?limit=50&offset=0"[
{
"id": "con_01hvghi",
"email": "user@example.com",
"last_engagement_at": "2026-01-10T09:22:00Z",
"sunset_stage": "warned",
"engagement_tier": "dormant"
}
]Contact scoring rules
Scoring rules define how lead-engagement points are awarded or subtracted when contact events occur.
/api/contacts/scoring-rulesReturns all scoring rules for the account.
Response fields
dataArray<Rule>Each rule includes id, name, description, event_type, condition, score_delta, is_active, priority, and timestamps.
{
"data": [
{
"id": "rule_01hv...",
"name": "Opened an email",
"description": "Award points when a contact opens an email",
"event_type": "email_opened",
"condition": {},
"score_delta": 5,
"is_active": true,
"priority": 100
}
]
}Create a scoring rule
/api/contacts/scoring-rulesCreate a scoring rule.
Request body
namestringbodyrequiredRule name (1–100 chars).
event_typestringbodyrequiredOne of email_opened, email_clicked, email_bounced, unsubscribed, form_submitted, tag_added, days_inactive, purchase_made.
score_deltaintegerbodyrequiredPoints to add (negative subtracts), −100 to 100.
descriptionstringbodyOptional description (≤500 chars).
conditionobjectbodydefault: {}Optional match condition for the event.
is_activebooleanbodydefault: trueWhether the rule is active.
priorityintegerbodydefault: 0Evaluation priority (0–1000).
Response fields
dataobjectThe created rule.
curl -X POST /api/contacts/scoring-rules \
-H "Content-Type: application/json" \
-d '{
"name": "Clicked a link",
"event_type": "email_clicked",
"score_delta": 10,
"priority": 100
}'{
"data": {
"id": "rule_01hv...",
"name": "Clicked a link",
"event_type": "email_clicked",
"score_delta": 10,
"is_active": true,
"priority": 100
}
}Update a scoring rule
/api/contacts/scoring-rulesUpdate a rule. Pass the rule id in the body with any fields to change.
Request body
idUUIDbodyrequiredID of the rule to update.
Any field accepted by POST (name, event_type, score_delta, description, condition, is_active, priority) may also be sent to update it.
Response fields
dataobjectThe updated rule.
curl -X PATCH /api/contacts/scoring-rules \
-H "Content-Type: application/json" \
-d '{ "id": "rule_01hv...", "score_delta": 15 }'Delete a scoring rule
/api/contacts/scoring-rulesDelete a rule. The rule id is passed as a query parameter.
Query parameters
idUUIDqueryrequiredID of the rule to delete.
Response fields
successbooleantrue when the rule was deleted.
curl -X DELETE "/api/contacts/scoring-rules?id=rule_01hv..."{ "success": true }Status codes
- 200 — OK. Run, stats, list, update, or delete succeeded.
- 201 — Created. Scoring rule created.
- 400 — Bad request. Invalid input (
Invalid input), invalid ruleid, or invalid JSON body. - 401 — Unauthorized. No active session.
- 404 — Not found. Scoring rule not found.
- 422 — Unprocessable entity. Scoring-rule body failed schema validation.
Recommended win-back flow
- Run
POST /api/list-health/run(ordryRun: truefirst) to refresh sunset stats. - Check
GET /api/list-health/stats— notecurrentStats.unengagedand the sunset counters. - Pull the unengaged contacts via
GET /api/contacts/health/unengaged. - Enroll the ones worth recovering with
POST /api/list-health/win-back. - Re-run the health check later — contacts who re-engage advance out of the sunset stages.
Contacts who do not respond to the win-back workflow are candidates for suppression. Continuing to send to chronically unengaged addresses harms your sender reputation.