> ## 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.

# Churn-risk scoring: read, segment, and act on per-contact risk

> Read the per-contact churn-risk score the nightly pipeline persists, threshold it into a high-risk retention segment, and hydrate that segment as a campaign audience — plus the polling cadence and the failure modes to plan for.

# Churn-risk scoring

Churn-risk scoring answers the retention question: "which active contacts are most likely to disengage?" A nightly scoring pipeline writes one churn-risk probability per contact, and Orbit refreshes it on every tick. This guide covers reading the score for one contact or a ranked list, thresholding it into a high-risk segment, hydrating that segment as a campaign audience, and the cadence and failure modes to build around. For the underlying model and its training loop, see [CDP predictive models](/guides/cdp-predictive-models).

## 1. What churn-risk returns

Every scored contact carries a `churn_risk` value: the probability, from 0 to 1, that the contact disengages, as computed by the trained churn-propensity model. Higher means higher risk. Each row also echoes a `segment_label` (the derived health label), `computed_at` (the timestamp of the scoring pass that produced it), and the companion scores from the same pass (buying intent, propensity, lifetime value estimate).

The score is precomputed by the nightly scoring pipeline — the reads below hit the persisted scores directly, so they return without fitting a model at request time. The server-owned risk bands classify the same probability everywhere in the product, so a contact flagged as high-risk in the API matches the dashboard notification:

| Band       | Score range    |
| ---------- | -------------- |
| `critical` | 0.70 and above |
| `high`     | 0.40 – 0.69    |
| `medium`   | 0.15 – 0.39    |
| `low`      | below 0.15     |

The 0.70 line is also the default materialization threshold and the same "high churn risk" cutoff the send-time decisioning gate uses, so an audience gated at ≥ 0.70 means the same thing in the API, the dashboard, and campaign send-time checks.

## 2. Prerequisites

* Access: the per-contact and cohort reads require an API key with the `contacts:read` scope; saving a segment requires `contacts:write` (plus an owner, admin, or developer role on dashboard-driven flows).
* Data: contact profiles must be receiving events and messages. The nightly scorer fits on first-party behavior (recency, message counts, event volume and variety); a workspace with no event flow has nothing to score.
* Model: the churn-propensity model ships in the built-in model catalog. One training run on your workspace's resolved outcomes is enough for the nightly pipeline to refresh scores from then on.

## 3. Read the score per contact

A single contact's latest score snapshot rides on the contact profile response:

```bash theme={null}
curl "https://api.orbit.devotel.io/api/v1/contacts/<contact_id>/profile" \
  -H "X-API-Key: dv_live_sk_your_key_here"
```

The response carries a `score` object with the raw probability and the derived band:

```json theme={null}
{
  "score": {
    "churn_risk": 0.82,
    "intent_score": 0.31,
    "propensity_score": 0.44,
    "ltv_estimate_cents": 12500,
    "segment_label": "at_risk",
    "computed_at": "2026-08-31T02:14:05.000Z",
    "propensity_segments": {
      "churn_risk_band": "critical",
      "clv_tier": "high"
    }
  }
}
```

`score` is `null` until the contact's first scoring pass completes — read the failure-modes section before treating null as "low risk."

For a ranked list instead of a single lookup, page through the scored base — sorted highest-risk-first by default:

```bash theme={null}
curl "https://api.orbit.devotel.io/api/v1/contacts/scores?limit=25" \
  -H "X-API-Key: dv_live_sk_your_key_here"
```

Each row carries `contact_id`, `churn_risk`, the companion scores, `segment_label`, `computed_at`, and the contact's display identity. Page with `next_cursor` from `meta.pagination`, override ordering with `sort` (for example `sort=-churn_risk` or `sort=intent_score`), or filter to one derived label with `segment_label`. For the push-style ranking over the trained model rather than the persisted table, `POST /api/v1/cdp/predictive-models/churn_propensity/score` re-ranks the scoring population on demand — see [CDP predictive models](/guides/cdp-predictive-models).

## 4. Build a segment on the threshold

Before you commit to a cutoff, size the base across the risk bands:

```bash theme={null}
curl "https://api.orbit.devotel.io/api/v1/cdp/churn-risk/bands" \
  -H "X-API-Key: dv_live_sk_your_key_here"
```

```json theme={null}
{
  "data": {
    "total_score_rows": 4210,
    "bands": [
      { "band": "critical", "min_score": 0.7, "count": 86 },
      { "band": "high", "min_score": 0.4, "count": 301 },
      { "band": "medium", "min_score": 0.15, "count": 942 },
      { "band": "low", "min_score": 0, "count": 2881 }
    ]
  }
}
```

Then dry-run the cohort a threshold would capture. The preview returns the full cohort size plus a ranked sample of the highest-risk contacts:

```bash theme={null}
curl -X POST "https://api.orbit.devotel.io/api/v1/cdp/churn-risk/segment/preview" \
  -H "X-API-Key: dv_live_sk_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{ "min_score": 0.7, "limit": 25 }'
```

```json theme={null}
{
  "data": {
    "preview": true,
    "min_score": 0.7,
    "cohort_size": 86,
    "limit": 25,
    "count": 25,
    "sample": [
      {
        "contact_id": "ct_01H...",
        "churn_risk": 0.93,
        "band": "critical",
        "display_name": "Maya Chen",
        "phone": "+15551234567",
        "email": "maya@example.com",
        "computed_at": "2026-08-31T02:14:05.000Z"
      }
    ]
  }
}
```

Omit `min_score` to use the canonical 0.7 high-risk line. The preview always re-reads the live scores, so the cohort you save matches what you sampled.

## 5. Materialize the segment and hydrate a campaign audience

Saving the cohort writes a static segment that campaign and journey audience pickers can target directly:

```bash theme={null}
curl -X POST "https://api.orbit.devotel.io/api/v1/cdp/churn-risk/segment" \
  -H "X-API-Key: dv_live_sk_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "min_score": 0.7,
    "name": "Churn risk ≥ 0.7 — retention",
    "description": "Above-threshold churn cohort for the win-back journey."
  }'
```

```json theme={null}
{
  "data": {
    "materialized": true,
    "min_score": 0.7,
    "segment": { "id": "seg_01H...", "name": "Churn risk ≥ 0.7 — retention" },
    "member_count": 86,
    "cohort_size": 86,
    "activation_ready": true
  }
}
```

The segment is a snapshot: membership is frozen at save time so a retention blast goes to exactly the cohort you previewed. The nightly scorer keeps rewriting the underlying scores — re-preview and re-save on your cadence to pick up new above-threshold contacts, or drop the materialized segment into a campaign audience as-is for a one-off blast. Target the saved segment from campaigns, journeys, or audience exports the same way as any other segment; see [Campaign end-to-end](/guides/campaign-end-to-end) for the send side.

## 6. Cadence: poll daily, never from a webhook hot path

Scores refresh overnight, so polling more often than once a day buys you nothing. Three patterns to avoid:

* **Don't read scores in a webhook handler.** The nightly tick rewrites every row; a contact event you react to at 03:00 still returns yesterday's score. Gate real-time flows on the persisted risk band label, or react on the event itself and let the overnight tick re-rank the contact.
* **Don't refit the model on demand in a loop.** The predictive-models score endpoint ranks on a cached fit; the persisted-table reads above are the cheap path and the one the dashboard uses.
* **Pick one read per use case.** Per-contact profile reads for one-off lookups (support tooling, a record page), the ranked `/contacts/scores` list for dashboards and exports, the threshold preview/materialize pair for audiences.

The nightly materialization cadence from section 5 stands in for scheduling: re-run preview → segment each morning and your retention audience stays current without touching the send path.

## 7. Failure modes

* **Score is `null` (low event coverage).** A contact with no score row returns `score: null` on the profile read and appears at the bottom of the sorted list. This is a data-coverage signal — the contact has no usable behavior yet — not a low-risk verdict. Wait for the next scoring pass, and treat a null-safe default in any downstream logic.
* **Stale timestamps.** `computed_at` always reflects the last completed scoring pass. During a paused or delayed tick the values stay readable but age; compare `computed_at` against your poll time and fall back to "no current risk signal" when it is older than a day.
* **Scope or role errors.** The reads require `contacts:read`; the materialization write requires `contacts:write`. A `403` names the missing scope — add it to the API key rather than retrying. Rate limits cap the churn-risk endpoints at 20 requests per minute per workspace, so back off on `429`.
* **Validation errors.** `min_score` outside \[0, 1] or a malformed body is a `400` with the reason. Thresholds are probabilities, not percentages — send `0.7`, not `70`.
* **Cohort over the materialization cap.** Saving a threshold that matches more than 500 contacts returns `422 COHORT_TOO_LARGE`. Raise `min_score`, or narrow the audience with the [Segments reference](/api-reference/segments) filters instead of the raw threshold.
* **Empty cohort.** When no contact is above the threshold, the materialization response is `materialized: false` with `reason: "no_profiles_above_threshold"` and nothing is persisted — lower the threshold or wait for the next tick.

## Dashboard twin

The same endpoints back **Audience → Churn risk** in the dashboard: the band distribution, the threshold preview, and the one-click segment save. Anything you can do over the API you can do there, and the page never exposes a capability the API hides.

## See also

* [CDP predictive models](/guides/cdp-predictive-models) — train, score, and schedule the churn-propensity model itself
* [CDP audiences](/guides/cdp-segments) — segments, filters, membership, and activation
* [Campaign end-to-end](/guides/campaign-end-to-end) — target the saved segment with a retention send
* [Segments API reference](/api-reference/segments) — filter-based narrowing of a threshold cohort
* [CDP API reference](/api-reference/endpoints/cdp) — the full churn-risk / predictive-models endpoint contracts
