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

# Read the per-campaign dialer sentiment & outcome card

> Understand the campaign-detail sentiment rollup — the positive/neutral/negative split, the average sentiment score, the per-carrier-outcome breakdown — and how to triage a negative-skew spike on an outbound dialer campaign.

# Per-campaign dialer sentiment & outcome card

The sentiment card on a dialer campaign's detail page tells you how answered calls actually went — not just whether they connected. It reads the post-call AI synthesis that [post-call scoring](/guides/outbound-dialer-campaign#post-call-ai-scoring-dialer-parity) writes onto every completed dialer call and rolls it up into an operator view: a positive/neutral/negative split, an average sentiment score, and a per-carrier-outcome breakdown.

Open it before every campaign review. A campaign can post a healthy connect rate and still be trending negative — the card is where you see that.

## What the card shows

The card renders three read-outs from `GET /api/v1/dialer/campaigns/:id/sentiment`:

* **The four-way tally** — the share of attempts classified `positive`, `neutral`, `negative`, plus a **Pending score** bucket for calls whose post-call scoring hasn't landed yet. Each bucket shows its count and, where scored, its mean sentiment score.
* **Average sentiment score** — a single mean over all scored attempts on a −1.0 to +1.0 scale, shown to two decimal places. A campaign with no scored attempts yet renders an em dash, never `0.00` — a literal zero would read as "dead-neutral," which is wrong for an unscored population.
* **Per-outcome breakdown** — every carrier outcome the campaign produced (`connected`, `no_answer`, `busy`, `voicemail`, `amd_detected`, in-flight `pending`, and so on), each with its positive/neutral/negative/pending-score counts and mean score. Rows sort by attempt volume, so the outcome most of your traffic produced leads the table.

A campaign that hasn't originated any attempts shows "No calls yet" rather than a zeroed table.

## How the aggregation works — and why hangup bias is avoided

Two things every attempt must clear before it enters the rollup:

1. The pacing scheduler stamps one `call_logs` row per originated attempt, keyed off the attempt's call identifier.
2. The post-call synthesis service scores that row — `sentiment_label` (`positive` / `neutral` / `negative`) plus `sentiment_score` (float from −1 to +1).

The aggregate then joins each attempt's carrier outcome (from the carrier/compliance tracking surface) to that scored call row and takes, per dialed contact, the **newest attempt** — so on a retry ladder only the final originated attempt counts, and every scored call counts exactly once.

Two consequences of that design matter when you read the card:

* **Unanswered outcomes don't drag the score down.** Without the join, unanswered attempts would all pile into a "negative" bucket purely because the call hung up before anyone spoke. Because the join only counts attempts with a real scored call row, `no_answer` / `busy` outcomes sit in their own rows with a pending-score count, and the sentiment split reflects only calls that actually connected.
* **Scoring lag shows up honestly.** A call whose synthesis hasn't landed yet folds into Pending score rather than being silently and wrongly scored as neutral. If Pending score dominates the card ten minutes after a batch ran, the scoring pipeline is behind — don't read the split as neutral-heavy.

## Where the card lives

Open **Outbound → Dialer → pick a campaign**. The card sits on the campaign detail page alongside the other campaign-analytics panels (live stats, caller-ID coverage). It refreshes on the page's polling cycle and covers the rolling 30-day window the aggregate reports.

A note on scope: the card is a read-only rollup. It never originates a call and never touches routing — like every dialer voice path, origination stays on the wholesale voice route.

## Triaging a negative-skew spike

A negative-skew spike means: the `negative` bucket gained share against `neutral`/`positive` across a window, or the average score fell while volume stayed normal. Work the spike in this order:

1. **Check the target segment.** Pull the current list's segment/filters. A list refresh that re-queued previously burned contacts, or a geographic/demographic shift in the audience, reads as negative skew the moment it lands — before any campaign-side change is in play. If the segment changed, re-screen the list and pause.
2. **Check the content.** Open the campaign's script and the recent audio/broadcast asset. A new offer line, a changed opening question, or a recording rework correlates with a skew step-change the day it shipped. If the skew starts at a content change, revert or rewrite that change before touching anything else.
3. **Check RNG pacing.** Look at the predictive pacing knobs and the abandon ceiling. A pacing ratio that creeps up puts calls in front of agents with less setup time, agents rush the opener, and recipients push back — that cascade shows up as negative sentiment on connected calls. Also check the abandon-rate trend: rising abandonment often walks in with a negative skew because more calls land with no agent ready.
4. **Correlate with local caller-ID coverage.** Open the campaign's caller-ID coverage read on the same detail page. If the negative skew concentrates specifically where the origin number isn't local, the issue is recipient trust, not the pitch — raise coverage for the affected region and re-measure.

Read the per-outcome breakdown to see whether the skew lives in `connected` specifically or across every outcome. A skew only on `connected` is a script/agent problem; a skew spread over `voicemail` / `amd_detected` too usually means list quality or coverage, not content.

## Export the read — `GET /api/v1/dialer/campaigns/:id/sentiment`

The card is backed by a public endpoint you can pull into your BI layer or a nightly QA export. Response shape (trimmed):

```json theme={null}
{
  "data": {
    "campaign_id": "camp_abc123",
    "campaign_name": "Sales outbound — August",
    "analytics_window_days": 30,
    "attempts_scanned": 18540,
    "attempts_scored": 9120,
    "avg_score": 0.4,
    "tally": [
      { "sentiment": "positive", "count": 5270, "avg_score": 0.61 },
      { "sentiment": "neutral", "count": 3310, "avg_score": 0.04 },
      { "sentiment": "negative", "count": 540, "avg_score": -0.48 },
      { "sentiment": "unscored", "count": 9420, "avg_score": null }
    ],
    "by_outcome": [
      {
        "outcome": "connected", "total": 9120,
        "positive": 5270, "neutral": 3310, "negative": 540, "unscored": 0,
        "avg_score": 0.4
      },
      {
        "outcome": "no_answer", "total": 6800,
        "positive": 0, "neutral": 0, "negative": 0, "unscored": 6800,
        "avg_score": null
      }
    ],
    "attempts_scan_capped": false
  }
}
```

Fields worth noting:

* `avg_score` — `null` when nothing has been scored yet. Parse as nullable.
* `tally` — always all four buckets, zero-padded so your consumer doesn't need to handle missing keys.
* `by_outcome[].unscored` — the counts in a row sum: `total = positive + neutral + negative + unscored`.
* `attempts_scan_capped` — `true` when the campaign's 30-day attempt volume exceeded the server-side scan bound; the rollup then describes the most recent cap of attempts rather than the whole window. Treat that flag as coverage metadata in any report you build off this.

```bash theme={null}
curl -s "https://api.orbit.devotel.io/api/v1/dialer/campaigns/$CAMPAIGN_ID/sentiment" \
  -H "X-API-Key: $ORBIT_API_KEY" | jq '.data'
```

Schedule the export daily for campaigns in flight; a 7-day rolling window is long enough to smooth an agent-bad-day blip and short enough to still catch a pacing drift.

## Pair with the manual-dial view

The campaign card covers every originated attempt, but when you chase a negative spike to specific contacts, an agent can re-dial one of them manually through the dialer's manual-dial endpoint (`POST /api/v1/dialer/manual-dial`). Manual dial originates a single, agent-supplied number through one human click — deliberately not automated-sequencing — so it never inflates the campaign's automated-attempt pacing, and the platform stamps that origination so the audit trail can prove a human placed it.

The workflow: pull the worst-scoring contacts from `by_outcome` on the campaign card, hand the short list to the QA agent, and have the agent re-dial one number at a time while re-screening the script. The response comes back through the same post-call scoring, so the follow-up lands in the same four-way split on the card the next time the aggregate refreshes.

## See also

* [Launch an outbound dialer campaign](/guides/outbound-dialer-campaign) — the pacing, lists, dispositions, and post-call scoring the card rolls up
* [Dialer dispositions](/voice/dialer-dispositions) — the carrier-outcome vocabulary the per-outcome breakdown uses
* [Conversation intelligence](/concepts/conversation-intelligence) — how post-call synthesis, sentiment, and outcomes apply across inbound and outbound calls
* [Dialer API](/api-reference/dialer) — full request/response schema for the sentiment endpoint
