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

# Insights console map: the KPI surface, filters, and gauge semantics per tile

> Walk the eight KPI-heavy Insights consoles — sentiment, deliverability, attribution, retention, costs, LLM spend, anomalies, funnels — as one lifecycle: the KPI each tile answers, the filter surface it ships, the gauge semantics behind each score, and one worked read-plus-filter example per console.

# Insights console map

The **Insights** hub at `/insights` is a grid of roughly thirty-five tiles. The [Insights hub overview](/insights/overview) walks the whole grid top to bottom, and the [Hub map](/insights/hub-map) is the one-line-per-tile index you keep open next to the dashboard. This page is the layer between them: a per-console walkthrough of the eight KPI-heavy route families that the hub-map leaves as a one-row table cell — **Sentiment**, **Deliverability**, **Attribution**, **Retention**, **Costs**, **LLM spend**, **Anomalies**, and **Funnels**.

For each console below you get the one KPI the tile exists to answer, the filter surface it ships, the gauge semantics behind every score it renders, and one worked example that reads the tile then narrows it with a filter. Read the console map when you already know which tile to open and need to know what its numbers mean and how to narrow them; read the hub-map when you are still deciding which tile to open.

Every console below is a tenant-owned read surface — it renders for your workspace only and re-checks its role gate server-side, so the route never shows a tile a seat cannot open. Route paths stay as shipped; do not rename them when scripting exports.

## What the hub surfaces, and why the tiles mirror the lifecycle

The hub tiles are not a flat list — they sit in the order an analysis usually walks them, and the eight consoles this page covers sit at the points where a number needs a reason or a budget needs an owner. The lifecycle, in the order the hub lays it out:

1. **Measure what happened.** **Deliverability** and **Funnels** count the raw events — which destinations accepted the message, which CDP step the cohort reached — before any interpretation. Read them first when a rate moved and you do not yet know why.
2. **Explain the why behind the number.** **Sentiment** reads the same conversation corpus from the sentiment angle and pins the direction each channel is moving. Open it when a delivery or containment number moved and the next question is whether the customer experience moved with it.
3. **Attribute the credit.** **Attribution** allocates conversion credit across channels under five touch models — first-touch, last-touch, linear, time-decay, U-shaped. Open it when the conversion count is settled and the budget-allocation question is next.
4. **Confirm the win held.** **Retention** groups contacts by their entry event and counts returns over weeks. Open it after a comparison or campaign win to confirm the entry cohort stayed better past week one.
5. **Reconcile the budget.** **Costs** and **LLM spend** break the bill down per channel and per model — carrier spend versus AI-token spend — so a routing regression and an AI regression do not read as the same line. Open them when a bill moved and you need to name which half.
6. **Watch the tripwires.** **Anomalies** flags the usage spikes and drops the detector learned to expect, so the lifecycle loops without an open tab. Open it last to arm the watch on the metric that decided the read.

The eight consoles below are the read surfaces at those six points. Each one's guide (cross-linked at the end of its section) carries the per-tile troubleshooting anchors — undefined CTR denominators, stale rollups, window-label mismatches, empty attribution buckets, LLM-spend model mapping gaps — so this page does not repeat them.

## How to read each console section

Every console section below has the same four parts, in the same order:

* **The KPI it answers** — the one question the tile exists to answer, and the headline metric that answers it.
* **The filter surface it ships** — the controls the tile renders, and which one narrows the read versus which one re-scopes it.
* **Gauge semantics** — what each score, band, or direction arrow means, including the direction-aware badges (a top-quartile badge on escalation means *low*, not high).
* **Worked example** — one read of the tile followed by one filter that narrows it, with the exact control you touch.

## Sentiment — `/insights/sentiment`

### The KPI it answers

Where does customer sentiment sit per channel, and which way is it moving? The headline metric is the **sentiment polarity score** per channel — a signed value that sits above or below neutral — read next to the **direction arrow** that says whether the channel trended up or down over the window.

### The filter surface it ships

* **Window** (24 hours, 7 days, 30 days). Narrows the read — the polarity score recomputes per window, so pick before you compare. 30 days for a trend call, 7 days when you are reacting to this week's change.
* **Channel** (email, SMS, push, RCS, voice). Narrows the read to one channel; the default is all channels side by side.
* **Segment** (campaign, queue, agent). Re-scopes the read to one slice of the conversation corpus — a segment filter changes the population, not the window.

### Gauge semantics

The polarity score is signed: above neutral is positive, below is negative, and the magnitude is the strength. The direction arrow is independent of the sign — a channel can sit positive and still arrow down when this week's cohort pulled the trend lower. Read the sign for *where*, the arrow for *which way*. A channel with a high positive score and a down arrow is a leading indicator to watch, not a win to claim.

### Worked example

**Read.** Open **Sentiment**, leave the window at 30 days, and read the channel table. SMS sits at +0.4 with an up arrow; voice sits at +0.1 with a down arrow. The voice channel is positive but trending down — that is the channel to investigate first.

**Filter.** Narrow to voice with the **Channel** filter and switch the **Segment** filter to the queue that handled this week's voice traffic. The table re-renders for that segment's conversations; the down arrow on the queue- scoped read confirms the regression is concentrated in one queue, not across voice. Open [Sentiment portfolio reading](/guides/sentiment-portfolio-reading) for the per-channel troubleshooting anchors.

## Deliverability — `/insights/deliverability`

### The KPI it answers

Which destinations and routes cost deliveries, and where does the delivery rate sag? The headline metric is the **delivery rate** per destination-and-route pair — delivered divided by sent — read next to the **failure-share** column that names which destination carries the largest slice of the losses.

### The filter surface it ships

* **Window** (24 hours, 7 days, 30 days). Narrows the read; the delivery rate recomputes per window.
* **Channel** (email, SMS, push, RCS). Narrows the read to one transport.
* **Destination** (carrier or receiving domain). Re-scopes the read to one destination — the one to open when a single carrier or mailbox provider is the suspect.

### Gauge semantics

The delivery rate is a percentage of sent, not of attempted — messages that never left the platform because of a send-time gate count against the denominator only if they were sent. A rate that sagged while the sent count held means the destination started rejecting; a rate that sagged while the sent count dropped means fewer sends made it to the denominator. Read the rate next to the sent count before naming the failure mode. The failure-share column sums to 100 across the destinations that returned a non-delivery verdict.

### Worked example

**Read.** Open **Deliverability**, set the window to 7 days, and read the destination table. The delivery rate for one carrier sits at 82% while the rest hold above 97%; the failure-share column puts 64% of the week's losses on that one carrier.

**Filter.** Narrow to SMS with the **Channel** filter and narrow to that carrier with the **Destination** filter. The table re-renders for that one destination's SMS traffic; the delivery rate holds at 82% in the scoped read, confirming the sag is destination-specific, not channel-wide. Open [Deliverability reading](/guides/insights-deliverability-reading) for the per-destination anchor and the carrier-rejection drill-down.

## Attribution — `/insights/attribution`

### The KPI it answers

Which channels earn conversion credit, under each touch model? The headline metric is the **credit share** per channel — the percentage of conversions each channel takes — read under each of the five models the tile ships: **first-touch**, **last-touch**, **linear**, **time-decay**, and **U-shaped**.

### The filter surface it ships

* **Window** (7 days, 30 days, 90 days). Narrows the read — conversions recompute per window.
* **Model** (first-touch, last-touch, linear, time-decay, U-shaped). Re-scopes the read — the model choice decides which channel earns the budget, so it is part of the read, not a display toggle. Run the same window under two models before calling one channel the winner.
* **Lookback** (1–365 days, default 30). Re-scopes the read — attribution joins each conversion to touchpoints inside the lookback. Widen the lookback before changing the tracking plan.

### Gauge semantics

The credit share sums to 100 across channels that received at least one in-window touchpoint. A conversion with zero in-window touchpoints surfaces as **unattributed**, not dropped — if the unattributed share is large, touchpoints are arriving without a stitchable id (`contact_id` or `anonymous_id`), not vanishing. Read the unattributed share before the per-channel shares; a large unattributed bucket invalidates the per-channel ranking below it. For a channel-to-channel read, `linear` levels the field and `last_touch` finds the closer — the model name is part of the number.

### Worked example

**Read.** Open **Attribution**, set the window to 30 days and the model to `last_touch`, and read the channel table. Email takes 48% of the credit; SMS takes 31%; the unattributed share sits at 6%.

**Filter.** Switch the **Model** filter to `linear` and re-read the same window. Email drops to 39% and SMS rises to 38% — under linear the field levels, so the email lead-in and the SMS closer split the credit more evenly. The two-model read is the decision input: if the budget follows the closer, `last_touch` holds; if it follows the assist, `linear` is the read. Open [Multi-touch attribution](/guides/multi-touch-attribution) for the model semantics and the lookback anchor.

## Retention — `/insights/retention`

### The KPI it answers

Do entry cohorts come back — did the improvement hold after week one? The headline metric is the **return rate** per cohort, plotted over the weeks since the entry event — a campaign send, a first conversation — so the curve shows whether the cohort stayed better past the first week or decayed by week two.

### The filter surface it ships

* **Window** (30 days, 90 days, 180 days). Narrows the read — the cohort window sets how far back the entry events reach.
* **Entry event** (campaign sent, first conversation, first purchase). Re-scopes the read — the entry event defines the cohort population, so a 7-day cohort off a campaign send and a 30-day cohort off a first conversation describe different populations by design.
* **Segment** (campaign, channel, account tier). Re-scopes the read to one slice — open this when the curve splits and you need to know whether the decay concentrates in one segment.

### Gauge semantics

The return rate is the share of the entry cohort that returned in a given week since entry — week 1 is the first return, week 2 the second, and so on. A curve that holds flat past week 1 confirms the comparison win survived its entry cohort; a curve that drops at week 2 says the win evaporated after the first return. Read the curve shape before any single week's number — the shape is the decision, the week's value is the evidence.

### Worked example

**Read.** Open **Retention**, set the window to 90 days and the entry event to `campaign sent`, and read the cohort curve. The week-1 return rate is 22%; week 2 drops to 9%; week 3 holds at 8%. The curve splits at week 2 — the entry cohort returned once and then decayed.

**Filter.** Narrow to one account tier with the **Segment** filter and re-read the curve. The tier's week-2 return rate holds at 18% while the rest dropped — the decay concentrated in the other tiers, not in this one. The segment-scoped read points at a segment-specific regression, not a global one. Open [Cohort retention](/guides/insights-cohort-retention) for the entry-event and window alignment anchor.

## Costs — `/insights/costs`

### The KPI it answers

Where does the budget go per channel, per destination? The headline metric is **spend** per channel, broken down by destination within each channel, read next to the **share-of-spend** column that names which destination carries the largest slice of the bill.

### The filter surface it ships

* **Window** (24 hours, 7 days, 30 days). Narrows the read; spend recomputes per window.
* **Channel** (email, SMS, push, RCS, voice). Narrows the read to one transport.
* **Destination** (carrier or receiving domain). Re-scopes the read — open this when one destination's line moved and the rest held.

### Gauge semantics

Spend is the carrier cost the platform billed for the window — not the AI-token cost, which sits on the LLM-spend tile. A bill that rose on the carrier line and fell on the model line is a routing regression, not an AI regression; read **Costs** and **LLM spend** as a pair before naming which half moved. The share-of-spend column sums to 100 across destinations that billed in the window. A destination with a flat spend but a rising share means the rest of the bill shrank — read the share next to the absolute spend before naming the line.

### Worked example

**Read.** Open **Costs**, set the window to 30 days, and read the channel table. SMS is the largest line; within SMS, one carrier takes 71% of the spend. The carrier's spend rose this month while the rest of SMS held.

**Filter.** Narrow to SMS with the **Channel** filter and to that carrier with the **Destination** filter. The table re-renders for that one destination's SMS spend; the spend holds in the scoped read, confirming the rise is destination-specific. Cross to **LLM spend** on the same window to confirm the AI-token line did not move — if it held, the rise is carrier cost, not model cost. Open [Channel costs](/guides/insights-costs) for the per-destination breakdown and the cost-economics walkthrough.

## LLM spend — `/insights/llm-spend`

### The KPI it answers

Where do AI tokens flow per model, and what is the bill per agent? The headline metric is **token spend** per model, broken down by agent within each model, read next to the **cost-per-conversation** column that normalizes the bill against the conversation count each agent handled.

### The filter surface it ships

* **Window** (24 hours, 7 days, 30 days). Narrows the read; token spend recomputes per window.
* **Model** (the LLM models your agents run on). Narrows the read to one model.
* **Agent** (the AI agents in your workspace). Re-scopes the read — open this when one agent's line moved and the rest held.

### Gauge semantics

Token spend is the AI cost the platform billed for the window — the model line, not the carrier line. The cost-per-conversation column divides an agent's spend by its conversation count, so a model that costs more per conversation is visible even when its absolute spend is small. Read the cost-per-conversation next to the conversation count before naming an agent expensive — a high cost-per-conversation on four conversations is provisional; the tile prints the count next to each agent name. A model whose spend rose while its conversation count held means the per- prompt cost moved; a model whose spend rose with its conversation count means the traffic moved. The two are different regressions.

### Worked example

**Read.** Open **LLM spend**, set the window to 30 days, and read the model table. One model is the largest line; within it, one agent's cost-per-conversation is 40% higher than the rest. The conversation count next to that agent is 12,431 — high enough to act on.

**Filter.** Narrow to that model with the **Model** filter and to that agent with the **Agent** filter. The table re-renders for the one agent's spend under the one model; the cost-per-conversation holds in the scoped read, confirming the per-prompt cost moved, not the traffic. Cross to **Costs** on the same window to confirm the carrier line held — if it did, the rise is model cost, not routing cost. Open [LLM spend](/guides/insights-llm-spend) for the per-model mapping anchor and the cost-economics walkthrough.

## Anomalies — `/insights/anomalies`

### The KPI it answers

Which usage spikes or drops did the anomaly detector flag? The headline metric is the **z-score** per flagged signal — how many standard deviations the observed value sat from the 28-day learned baseline — read next to the **direction** column that says whether the signal spiked up or dropped down.

### The filter surface it ships

* **Signal** (delivery rate, outbound volume, spend, latency). Narrows the read to one transport or experience signal.
* **Severity** (all, high, medium, low). Narrows the read to the anomalies worth triaging first.
* **Status** (open, acknowledged, resolved). Re-scopes the read — open this to see only the anomalies still awaiting a verdict.

### Gauge semantics

The z-score is against a 28-day learned baseline, not a fixed threshold — the detector learns each signal's normal range per tenant and flags deviations from it, so the same z-score means different absolute values on two tenants. A positive z-score with an up direction is a spike; a negative z-score with a down direction is a drop. Read the z-score next to the direction before naming the failure mode — a spike on delivery rate is a different regression than a drop on outbound volume, and the same z-score on both is a coincidence to investigate, not a verdict. An anomaly is open until a seat acknowledges or resolves it; the status filter is the triage queue.

### Worked example

**Read.** Open **Anomalies**, leave the signal at all, and set the severity to high. The table lists three high-severity anomalies; the top row is a delivery-rate spike at z = 4.2, up direction, open status.

**Filter.** Narrow to delivery rate with the **Signal** filter and to open with the **Status** filter. The table re-renders for the one signal's open anomalies; the z = 4.2 spike is the top row. Open [Usage anomaly alert rules](/guides/usage-anomaly-alert-rules) to arm a rule that fires the next time the same signal crosses, and [Usage anomaly alert rules](/guides/insights-anomalies-ledger-reading) for the ledger-reading walkthrough that traces the spike back to its first occurrence.

## Funnels — `/insights/funnels`

### The KPI it answers

Where between two defined steps do CDP events drop off? The headline metric is the **drop-off rate** per step — the share of the cohort that reached the previous step but not the next — read as a column down the funnel so the step that loses the most cohort is the one that names the regression.

### The filter surface it ships

* **Steps** (2–8 CDP event names, in order). Re-scopes the read — the step list is the funnel definition, so changing it changes the population. Pick the steps before the window.
* **Window** (7 days, 14 days, 30 days). Narrows the read — the cohort enters inside the window.
* **Segment** (campaign, channel, account tier). Re-scopes the read to one slice — open this when one step's drop-off concentrates in one segment.

### Gauge semantics

The drop-off rate is per step, not cumulative — it measures the loss from the previous step to the current one, so the step with the highest drop-off rate is the step that loses the most cohort at that point. The cumulative conversion rate is the share of the entry cohort that reached the current step, read alongside the drop-off rate to separate *where the loss happened* from *how far the cohort got*. A funnel with a high drop-off at step 2 and a low cumulative conversion at step 3 is telling you the loss is between steps 1 and 2, not at the end. Read the drop-off rate for *where*, the cumulative conversion for *how far*.

### Worked example

**Read.** Open **Funnels**, set the steps to `Campaign Sent` → `Message Clicked` → `Checkout Completed`, and set the window to 14 days. The drop-off rate at step 2 is 80% — 80% of the cohort that received the message did not click; the cumulative conversion at step 3 is 4%.

**Filter.** Narrow to one campaign with the **Segment** filter and re-read the funnel. The drop-off at step 2 holds at 80% for the campaign-scoped read, but the cumulative conversion at step 3 drops to 2% — the campaign's cohort reached checkout at half the rate of the funnel-wide read. The segment-scoped read names the campaign as the regression source. Open [Funnels over CDP events](/guides/insights-funnels-cdp-conversion) for the step-definition anchor and the funnel-versus-attribution divergence note.

## RBAC — which consoles render for which role

Three gating clusters live across the eight consoles; the destination page re-checks each one server-side, so the route never shows a console a seat cannot open:

* **Finance cluster — Costs, LLM spend.** Render only for owner / admin / developer / billing seats. A viewer or agent sees neither tile and the destination page 403s the seat regardless — the grid simply avoids showing a surface a role cannot read.
* **CDP cluster — Retention, Funnels.** Render only for owner / admin / developer, mirroring the read gate over the PII-bearing customer-event stream. A billing-only seat does not see these either.
* **Open read — Sentiment, Deliverability, Attribution, Anomalies.** Render for every member, including viewers, because the read route is genuinely member-readable. Writes on anomaly alert rules stay owner / admin / developer-gated at the API, but the read views are open.

The role-gating summary on the [Insights hub overview](/insights/overview#permissions-what-a-viewer-can-and-cannot-reach) carries the same allow-list spelled out per seat; this page mirrors it so a role-scoping question does not bounce between two pages.

## Worked API samples — the eight consoles over the Insights API

Every console above is scriptable: the same read the dashboard tile renders returns over the [Insights API](/api-reference/insights) as JSON, so a BI tool or scheduled job drives the exact numbers a tile shows without opening the dashboard. The samples below run as written — base `https://api.orbit.devotel.io/api/v1`, `X-API-Key` on every call, the `{ data, meta }` success envelope back. Every Insights call shares one per-tenant budget of 60 requests / minute and needs the `analytics:read` scope; on a 429, honour `Retry-After` and back off.

### Read the sentiment portfolio

Returns the polarity score and direction per channel for the window.

<CodeGroup>
  ```bash cURL theme={null}
  curl -G "https://api.orbit.devotel.io/api/v1/insights/sentiment" \
    -H "X-API-Key: dv_live_sk_..." \
    --data-urlencode "window=30d" \
    --data-urlencode "channel=sms"
  ```

  ```typescript Node.js theme={null}
  import { Devotel } from "@devotel-orbit/node";

  const orbit = new Devotel({ apiKey: process.env.ORBIT_API_KEY });

  const sentiment = await orbit.request<{ data: { channels: Array<{ channel: string; polarity: number; direction: "up" | "down" }> } }>(
    "GET",
    "/insights/sentiment?window=30d&channel=sms",
  );

  console.log(sentiment.data.channels[0].polarity, sentiment.data.channels[0].direction);
  ```

  ```python Python theme={null}
  from devotel_orbit import Devotel

  client = Devotel(api_key=os.environ["ORBIT_API_KEY"])

  sentiment = client.request(
      "GET",
      "/insights/sentiment",
      params={"window": "30d", "channel": "sms"},
  )

  print(sentiment["data"]["channels"][0]["polarity"])
  ```
</CodeGroup>

### Read the deliverability table

Returns the delivery rate and failure share per destination for the window and channel.

<CodeGroup>
  ```bash cURL theme={null}
  curl -G "https://api.orbit.devotel.io/api/v1/insights/deliverability" \
    -H "X-API-Key: dv_live_sk_..." \
    --data-urlencode "window=7d" \
    --data-urlencode "channel=sms"
  ```

  ```typescript Node.js theme={null}
  import { Devotel } from "@devotel-orbit/node";

  const orbit = new Devotel({ apiKey: process.env.ORBIT_API_KEY });

  const deliverability = await orbit.request<{ data: { destinations: Array<{ destination: string; deliveryRate: number; failureShare: number; sent: number }> } }>(
    "GET",
    "/insights/deliverability?window=7d&channel=sms",
  );

  console.log(deliverability.data.destinations[0].deliveryRate);
  ```

  ```python Python theme={null}
  from devotel_orbit import Devotel

  client = Devotel(api_key=os.environ["ORBIT_API_KEY"])

  deliverability = client.request(
      "GET",
      "/insights/deliverability",
      params={"window": "7d", "channel": "sms"},
  )

  print(deliverability["data"]["destinations"][0]["delivery_rate"])
  ```
</CodeGroup>

### Read attribution under a model

Returns the credit share per channel for the window, model, and lookback.

<CodeGroup>
  ```bash cURL theme={null}
  curl -G "https://api.orbit.devotel.io/api/v1/insights/attribution" \
    -H "X-API-Key: dv_live_sk_..." \
    --data-urlencode "window=30d" \
    --data-urlencode "model=linear" \
    --data-urlencode "lookback_days=30"
  ```

  ```typescript Node.js theme={null}
  import { Devotel } from "@devotel-orbit/node";

  const orbit = new Devotel({ apiKey: process.env.ORBIT_API_KEY });

  const attribution = await orbit.request<{ data: { channels: Array<{ channel: string; creditShare: number; conversions: number }>; unattributedShare: number } }>(
    "GET",
    "/insights/attribution?window=30d&model=linear&lookback_days=30",
  );

  console.log(attribution.data.channels[0].creditShare, attribution.data.unattributedShare);
  ```

  ```python Python theme={null}
  from devotel_orbit import Devotel

  client = Devotel(api_key=os.environ["ORBIT_API_KEY"])

  attribution = client.request(
      "GET",
      "/insights/attribution",
      params={"window": "30d", "model": "linear", "lookback_days": "30"},
  )

  print(attribution["data"]["channels"][0]["credit_share"])
  ```
</CodeGroup>

### Read the retention cohort curve

Returns the return rate per week since entry for the cohort window and entry event.

<CodeGroup>
  ```bash cURL theme={null}
  curl -G "https://api.orbit.devotel.io/api/v1/insights/retention" \
    -H "X-API-Key: dv_live_sk_..." \
    --data-urlencode "window=90d" \
    --data-urlencode "entry_event=campaign_sent"
  ```

  ```typescript Node.js theme={null}
  import { Devotel } from "@devotel-orbit/node";

  const orbit = new Devotel({ apiKey: process.env.ORBIT_API_KEY });

  const retention = await orbit.request<{ data: { weeks: Array<{ week: number; returnRate: number; cohortSize: number }> } }>(
    "GET",
    "/insights/retention?window=90d&entry_event=campaign_sent",
  );

  console.log(retention.data.weeks[1].returnRate);
  ```

  ```python Python theme={null}
  from devotel_orbit import Devotel

  client = Devotel(api_key=os.environ["ORBIT_API_KEY"])

  retention = client.request(
      "GET",
      "/insights/retention",
      params={"window": "90d", "entry_event": "campaign_sent"},
  )

  print(retention["data"]["weeks"][1]["return_rate"])
  ```
</CodeGroup>

### Read the channel-cost breakdown

Returns the spend and share-of-spend per destination for the window and channel.

<CodeGroup>
  ```bash cURL theme={null}
  curl -G "https://api.orbit.devotel.io/api/v1/insights/costs" \
    -H "X-API-Key: dv_live_sk_..." \
    --data-urlencode "window=30d" \
    --data-urlencode "channel=sms"
  ```

  ```typescript Node.js theme={null}
  import { Devotel } from "@devotel-orbit/node";

  const orbit = new Devotel({ apiKey: process.env.ORBIT_API_KEY });

  const costs = await orbit.request<{ data: { destinations: Array<{ destination: string; spend: number; shareOfSpend: number }> } }>(
    "GET",
    "/insights/costs?window=30d&channel=sms",
  );

  console.log(costs.data.destinations[0].spend, costs.data.destinations[0].shareOfSpend);
  ```

  ```python Python theme={null}
  from devotel_orbit import Devotel

  client = Devotel(api_key=os.environ["ORBIT_API_KEY"])

  costs = client.request(
      "GET",
      "/insights/costs",
      params={"window": "30d", "channel": "sms"},
  )

  print(costs["data"]["destinations"][0]["spend"])
  ```
</CodeGroup>

### Read the LLM-spend breakdown

Returns the token spend and cost-per-conversation per agent for the window and model.

<CodeGroup>
  ```bash cURL theme={null}
  curl -G "https://api.orbit.devotel.io/api/v1/insights/llm-spend" \
    -H "X-API-Key: dv_live_sk_..." \
    --data-urlencode "window=30d"
  ```

  ```typescript Node.js theme={null}
  import { Devotel } from "@devotel-orbit/node";

  const orbit = new Devotel({ apiKey: process.env.ORBIT_API_KEY });

  const llmSpend = await orbit.request<{ data: { models: Array<{ model: string; agents: Array<{ agent_id: string; spend: number; costPerConversation: number; conversationCount: number }> }> } }>(
    "GET",
    "/insights/llm-spend?window=30d",
  );

  console.log(llmSpend.data.models[0].agents[0].costPerConversation);
  ```

  ```python Python theme={null}
  from devotel_orbit import Devotel

  client = Devotel(api_key=os.environ["ORBIT_API_KEY"])

  llm_spend = client.request(
      "GET",
      "/insights/llm-spend",
      params={"window": "30d"},
  )

  print(llm_spend["data"]["models"][0]["agents"][0]["cost_per_conversation"])
  ```
</CodeGroup>

### Read the anomaly ledger

Returns the flagged anomalies with z-score and direction for the signal and status.

<CodeGroup>
  ```bash cURL theme={null}
  curl -G "https://api.orbit.devotel.io/api/v1/insights/anomalies" \
    -H "X-API-Key: dv_live_sk_..." \
    --data-urlencode "signal=delivery_rate" \
    --data-urlencode "status=open"
  ```

  ```typescript Node.js theme={null}
  import { Devotel } from "@devotel-orbit/node";

  const orbit = new Devotel({ apiKey: process.env.ORBIT_API_KEY });

  const anomalies = await orbit.request<{ data: { anomalies: Array<{ signal: string; zScore: number; direction: "up" | "down"; status: string }> } }>(
    "GET",
    "/insights/anomalies?signal=delivery_rate&status=open",
  );

  console.log(anomalies.data.anomalies[0].zScore, anomalies.data.anomalies[0].direction);
  ```

  ```python Python theme={null}
  from devotel_orbit import Devotel

  client = Devotel(api_key=os.environ["ORBIT_API_KEY"])

  anomalies = client.request(
      "GET",
      "/insights/anomalies",
      params={"signal": "delivery_rate", "status": "open"},
  )

  print(anomalies["data"]["anomalies"][0]["z_score"])
  ```
</CodeGroup>

### Read the funnel drop-off

Returns the drop-off rate and cumulative conversion per step for the step list and window.

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST "https://api.orbit.devotel.io/api/v1/cdp/analytics/funnel" \
    -H "X-API-Key: dv_live_sk_..." \
    -H "Content-Type: application/json" \
    -d '{
      "steps": ["Campaign Sent", "Message Clicked", "Checkout Completed"],
      "window_days": 14,
      "since": "2026-09-25T00:00:00Z",
      "until": "2026-10-09T00:00:00Z"
    }'
  ```

  ```typescript Node.js theme={null}
  import { Devotel } from "@devotel-orbit/node";

  const orbit = new Devotel({ apiKey: process.env.ORBIT_API_KEY });

  const funnel = await orbit.request<{ data: { steps: Array<{ step: string; index: number; reached: number; dropOffRateFromPrevious: number; cumulativeConversionRate: number }> } }>(
    "POST",
    "/cdp/analytics/funnel",
    {
      steps: ["Campaign Sent", "Message Clicked", "Checkout Completed"],
      window_days: 14,
      since: "2026-09-25T00:00:00Z",
      until: "2026-10-09T00:00:00Z",
    },
  );

  console.log(funnel.data.steps[1].dropOffRateFromPrevious, funnel.data.steps[2].cumulativeConversionRate);
  ```

  ```python Python theme={null}
  from devotel_orbit import Devotel

  client = Devotel(api_key=os.environ["ORBIT_API_KEY"])

  funnel = client.request(
      "POST",
      "/cdp/analytics/funnel",
      json={
          "steps": ["Campaign Sent", "Message Clicked", "Checkout Completed"],
          "window_days": 14,
          "since": "2026-09-25T00:00:00Z",
          "until": "2026-10-09T00:00:00Z",
      },
  )

  print(funnel["data"]["steps"][1]["drop_off_rate_from_previous"])
  ```
</CodeGroup>

## Deep-link list — a guide per console

Each console has one long-form guide; open the hub tile for the console and the guide for the workflow and the per-tile troubleshooting anchors:

| Console | Route | Guide |
| - | - | - |
| Sentiment | `/insights/sentiment` | [Sentiment portfolio reading](/guides/sentiment-portfolio-reading) |
| Deliverability | `/insights/deliverability` | [Deliverability reading](/guides/insights-deliverability-reading) |
| Attribution | `/insights/attribution` | [Multi-touch attribution](/guides/multi-touch-attribution) |
| Retention | `/insights/retention` | [Cohort retention](/guides/insights-cohort-retention) |
| Costs | `/insights/costs` | [Channel costs](/guides/insights-costs) |
| LLM spend | `/insights/llm-spend` | [LLM spend](/guides/insights-llm-spend) |
| Anomalies | `/insights/anomalies` | [Usage anomaly alert rules](/guides/usage-anomaly-alert-rules) · [Anomaly ledger reading](/guides/insights-anomalies-ledger-reading) |
| Funnels | `/insights/funnels` | [Funnels over CDP events](/guides/insights-funnels-cdp-conversion) |

## See also

* [Insights hub overview](/insights/overview) — the entry page through the whole grid, with the role-specific worked examples
* [Hub map](/insights/hub-map) — the one-line-per-tile index you keep open next to the dashboard
* [Read the Insights dashboards as one decision picture](/guides/insights-agent-comparison-cohort-loop) — the worked narrative behind the four-step decision loop
* [Cost economics of insights](/guides/cost-economics-insights) — the Cost and LLM-spend family walkthrough end to end
* [CDP BI-tool connectors](/guides/cdp-bi-connectors) — the pull path when your analytics team reads the same CDP data from Tableau, Power BI, Looker, Superset, or Metabase instead of the dashboard


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.