Skip to main content

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.

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: 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:
The response carries a score object with the raw probability and the derived band:
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:
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.

4. Build a segment on the threshold

Before you commit to a cutoff, size the base across the risk bands:
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:
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:
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 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 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