> ## Documentation Index
> Fetch the complete documentation index at: https://docs.orbit.devotel.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Contact & segment deliverability health

> Score deliverability health per contact and per segment on one channel, read the consent-suppression overlay, and gate campaigns and flows on an honest reachability split.

# Contact & segment deliverability health

The number-level health scores in [Number health](/numbers/health) tell you
how healthy your *senders* are. The per-contact deliverability health score
answers the complementary question: how healthy is it to send to **this
contact, on this channel** — and separately, whether consent lets you send
at all. The dashboard surfaces it as a health card on the contact detail
page, and two API endpoints expose the same verdict per contact and rolled
up across a segment.

## 1. What the score blends

The score reads the last 90 days of message traffic between you and the
contact on one channel (`sms`, `whatsapp`, `email`, or `voice`), and blends
five components into a raw value in \[0, 1] — 1 is healthiest:

* **Hard bounce share (weight 0.35)** — terminal failures that look
  permanent: `bounced` rows plus the soft-NACK error-code families
  (`unreachable`, `invalid number`, `no route`). Hard bounces drag the
  score to 0 once they reach 20% of attempted sends.
* **Soft bounce share (weight 0.15)** — other terminal failures
  (`failed` / `undelivered` / `rejected` minus the hard-bounce branches).
  Transient, clears on retry, so the blend forgives them up to a 50%
  share.
* **Complaint share (weight 0.25)** — reserved for per-contact complaint
  feeds. A complaint still blocks the send here, but as a consent overlay
  (section 3), not a raw-score penalty.
* **Engagement share (weight 0.15)** — inbound replies on the same
  channel, saturating once replies reach 10% of attempted sends.
* **Failure recency (weight 0.1)** — days since the newest terminal
  failure, decaying on a 14-day half-life. A fresh failure drags the
  score; a failure 60 days behind clean traffic barely matters.

Every component and its weight travels with the response, so you always
answer "why a 0.42?" from the `components` map — no black box.

## 2. The two endpoints

Both endpoints take an optional `?channel=` query param and default to
`sms` when omitted. Both are pure reads of persisted traffic — no live
provider call and nothing on the outbound path.

### Per contact

```bash theme={null}
curl "https://api.orbit.devotel.io/api/v1/contacts/ct_abc123/deliverability-health?channel=sms" \
  -H "X-API-Key: dv_live_sk_..."
```

```json theme={null}
{
  "data": {
    "contactId": "ct_abc123",
    "channel": "sms",
    "raw": {
      "rawScore": 0.92,
      "components": {
        "hard_bounce_share": { "weight": 0.35, "value": 1 },
        "soft_bounce_share": { "weight": 0.15, "value": 1 },
        "complaint_share": { "weight": 0.25, "value": 1 },
        "engagement_share": { "weight": 0.15, "value": 0.6 },
        "failure_recency": { "weight": 0.1, "value": 1 }
      }
    },
    "sendableScore": 0.92,
    "blockReason": null,
    "label": "healthy",
    "insufficientData": false
  }
}
```

Unknown contact ids return 404. Possible `label` values: `healthy`
(sendable ≥ 0.8), `watch` (≥ 0.5), `at_risk` (\< 0.5), `consent_blocked`
(suppressed or opted out), `insufficient_data` (no send signal yet).

### Per segment

```bash theme={null}
curl "https://api.orbit.devotel.io/api/v1/contacts/segments/seg_abc123/deliverability-health?channel=sms" \
  -H "X-API-Key: dv_live_sk_..."
```

```json theme={null}
{
  "data": {
    "segmentId": "seg_abc123",
    "channel": "sms",
    "memberCount": 3420,
    "truncated": false,
    "health": {
      "contactCount": 3420,
      "sendableHealth": 0.81,
      "overallHealth": 0.74,
      "suppressedCount": 180,
      "optedOutCount": 92,
      "insufficientDataCount": 211,
      "reachableShare": 0.92
    }
  }
}
```

Readings for the segment rollup:

* `sendableHealth` — mean raw health over the population you may actually
  send to. `null` when every member is consent-blocked.
* `overallHealth` — mean raw health across all members, consent-blocked
  or not.
* `suppressedCount` / `optedOutCount` — the consent split (section 3).
* `insufficientDataCount` — members with no send signal (section 6).
* `reachableShare` — share of the segment sendable on this channel, in
  \[0, 1]. A perfectly healthy segment can still be unreachable.

## 3. The consent-suppression overlay

The raw score answers "is this contact deliverable?"; the overlay answers
"may we send?". Conflating the two lies either to the operator (a
healthy-but-blocked contact shows as healthy) or to a compliance reviewer
(an opted-out send pretends to be a healthy send). The overlay folds three
sources into two booleans per (contact, channel):

* `suppressed` — the address is on the tenant's external suppression list
  (`suppression_list`, `channel IN ('all', <channel>)`, not revoked): a
  global unsubscribe, hard-bounce suppression, or complaint suppression.
  Overrides per-channel opt-in, and outranks opt-out when both exist.
* `optedOut` — an explicit opt-out on this channel, from the contact's
  `consent_records` ledger UNION the legacy
  `channel_preferences[channel].opted_out` flag.

When either is true, `sendableScore` is `0` and `blockReason` is
`"suppressed"` or `"opted_out"`; the raw score and components stay visible
for audit.

Two sources are deliberately **not** part of this overlay:

* **The hot-path STOP-response Redis fence** — a cached fast-reject over
  recent STOP traffic. Redundant with `consent_records` at read time and
  still checked authoritatively at dispatch; including it here would make
  an honest rollup depend on a cache's warm state.
* **The DNC import list** — unattached address rows the overlay cannot
  join to a stored contact; the send-time gate applies it on the
  destination address instead.

A contact with no identifier on the channel (no `email`, no `phone`) skips
the suppression probe and keeps its raw score — the send-time 422 on a
missing identifier is the authoritative block there.

## 4. Segment rollup cost

The segment endpoint serves up to 5,000 materialised members per read in a
constant number of grouped round-trips — the signal rollup plus one
batched consent-overlay query — regardless of cohort size. A 5,000-member
segment costs the same query shape as a 1-member one; members past the
5,000 cap set `truncated: true` in the response, and the member list the
endpoint scans comes from the materialised `contact_segment_members`
surface, not a re-run of the segment filter.

## 5. Where it appears in the dashboard

Open **Audience → Contacts**, pick a contact, and land on the **Overview**
tab. The **Deliverability health** card sits between the engagement panel
and the reachability card, with a channel selector across `sms`,
`whatsapp`, `email`, and `voice`. It renders the `{ label, score }` pair
plus the same consent-suppression overlay and insufficient-data handling
the API returns above.

## 6. Zero-state semantics

A contact with no outbound traffic on the channel scores `insufficientData: true`
and renders **No send signal yet — the first outbound traffic starts the
health record.** It never shows as a healthy 1.0: the raw score reads
perfect only because nothing has been observed yet, and the honesty
contract keeps that distinct from a proven-healthy contact. Segment
rollups count these contacts in `insufficientDataCount`.

## 7. Put it to work

* **Throttle a segment pre-campaign.** Pull the segment rollup before a
  send: a `reachableShare` under your floor means re-clean the segment or
  accept the suppression numbers before you spend; a low `sendableHealth`
  means tune targeting before a real blast damages sender reputation.
* **Gate a flow on a health threshold.** Poll the per-contact endpoint at
  a flow step and branch on `sendableScore` — route `at_risk` contacts to
  email or a re-permission segment rather than hammering a failing
  channel. Because scores are pure per-channel reads, a flow can probe
  each channel and pick the healthiest.

Related: [Number health, warming, and carrier reputation](/numbers/health)
for sender-side scores, and [Insights dashboards](/guides/insights-dashboards)
for the aggregate `/insights/deliverability` surface.
