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

# Signals

> GET /frustrations, /emotions, /tool-failures, and /hallucinations — signal detections with evidence and embedded conversation context.

These endpoints read the per-conversation signals Moda detects on ingested data: user frustration and other emotions, tool failures, and grounding (hallucination) results. Detections typically appear within minutes of ingest. The same data drives the [Signals dashboard](/dashboard/signals).

## GET /frustrations

Frustration detections with evidence, verbatim user quotes, and an embedded context window.

### Query parameters

<ParamField query="days_back" type="integer" default="7">
  1–90.
</ParamField>

<ParamField query="limit" type="integer" default="10">
  1–20. This endpoint's cap is 20, lower than the general 100.
</ParamField>

<ParamField query="offset" type="integer" default="0">
  0–10,000.
</ParamField>

### Response fields

<ResponseField name="summary" type="object">
  `total_analyzed` (conversations analyzed in the window), `frustrated_count`, `at_risk_count` (risk score at least 0.5 but not frustrated), `frustration_rate_pct`.
</ResponseField>

<ResponseField name="signal_breakdown" type="object">
  Map of frustration signal to count, across frustrated conversations only.
</ResponseField>

<ResponseField name="frustrations" type="array">
  Frustrated and at-risk conversations, ordered by `frustration_score` then `risk_score`. Each row has `conversation_id`, `is_frustrated`, `frustration_score`, `risk_score`, `trajectory`, `target`, `primary_cause`, `evidence`, `user_quotes[]` (`turn`, `quote`, `signal`), `expressed_signals[]`, `observed_signals[]`, `key_turns[]`, `message_count`, `detected_at`, and `conversation` — an embedded context window (same shape as [`/conversations/:id/context`](/data-api/conversations#get-conversationsidcontext)) centered on the first key turn, or `null`.
</ResponseField>

<ResponseField name="pagination" type="object">
  `limit`, `offset`, `total`, `has_more`.
</ResponseField>

### Example

```bash theme={"dark"}
curl "https://moda.dev/api/v1/data/frustrations?days_back=7&limit=1" \
  -H "x-api-key: YOUR_MODA_API_KEY"
```

```json theme={"dark"}
{
  "summary": {
    "total_analyzed": 1102,
    "frustrated_count": 43,
    "at_risk_count": 18,
    "frustration_rate_pct": 3.9
  },
  "signal_breakdown": { "exasperation": 21, "escalation_request": 12, "sarcasm": 7 },
  "frustrations": [
    {
      "conversation_id": "conv_9f2c41d08ab37e51",
      "is_frustrated": true,
      "frustration_score": 0.86,
      "risk_score": 0.91,
      "trajectory": "building",
      "target": "bot",
      "primary_cause": "Agent repeated the same help-page link instead of acting on the refund request.",
      "evidence": "User restated the request three times with escalating language before asking for a human.",
      "user_quotes": [
        { "turn": 6, "quote": "I already asked about the refund policy twice and the bot keeps linking the same page.", "signal": "exasperation" }
      ],
      "expressed_signals": ["exasperation", "escalation_request"],
      "observed_signals": [],
      "key_turns": [6],
      "message_count": 18,
      "detected_at": "2026-08-14T09:44:03",
      "conversation": {
        "conversation_id": "conv_9f2c41d08ab37e51",
        "total_messages": 18,
        "summary": "User asks how to request a refund for an annual plan; agent explains the policy and opens a ticket.",
        "context": {
          "center_index": 6,
          "from_index": 4,
          "to_index": 8,
          "messages": [
            { "index": 6, "role": "user", "content": "I already asked about the refund policy twice and the bot keeps linking the same page.", "tool_calls": [], "tool_results": [], "timestamp": "2026-08-14T09:31:22" }
          ]
        }
      }
    }
  ],
  "pagination": { "limit": 1, "offset": 0, "total": 61, "has_more": true }
}
```

## GET /emotions

Multi-family emotion detections. Moda scores six families — `frustration`, `sadness`, `confusion`, `anxiety`, `trust`, `positive` — with one detection row per conversation and family. See [Signals in the dashboard](/dashboard/signals) for the full 16-signal taxonomy.

### Query parameters

<ParamField query="days_back" type="integer" default="7">
  1–90.
</ParamField>

<ParamField query="limit" type="integer" default="10">
  1–20.
</ParamField>

<ParamField query="offset" type="integer" default="0">
  0–10,000.
</ParamField>

<ParamField query="family" type="string">
  Filter the paginated `detections` list (and its `total`) to one family. The `summary` and `signal_breakdown` always cover all families. Unknown values return `400`.
</ParamField>

### Response fields

<ResponseField name="summary" type="object">
  `conversations_analyzed`, `by_family` (detected count per family), `negative_rate_pct` (share of analyzed conversations with any negative-family detection), `positive_rate_pct`, and `repair_rate_pct` (share of negative detections whose trajectory resolved).
</ResponseField>

<ResponseField name="signal_breakdown" type="array">
  `{family, signal, count}` rows across detected conversations.
</ResponseField>

<ResponseField name="detections" type="array">
  Detected and at-risk rows, ordered by score. Each has `detection_id`, `conversation_id`, `user_id`, `family`, `is_detected`, `score`, `risk_score`, `trajectory`, `elicitor`, `primary_cause`, `evidence`, `user_quotes[]`, `expressed_signals[]`, `observed_signals[]`, `key_turns[]`, `downgrade_reason`, `detection_grade` (`rlm` for deep analyses, `light` for the positive-family fast path), `message_count`, `detected_at`, and `conversation_context` (embedded context window or `null`).
</ResponseField>

<ResponseField name="pagination" type="object">
  `limit`, `offset`, `total`, `has_more`.
</ResponseField>

### Example

```bash theme={"dark"}
curl "https://moda.dev/api/v1/data/emotions?days_back=7&family=confusion&limit=1" \
  -H "x-api-key: YOUR_MODA_API_KEY"
```

```json theme={"dark"}
{
  "summary": {
    "conversations_analyzed": 1102,
    "by_family": { "frustration": 43, "sadness": 4, "confusion": 22, "anxiety": 9, "trust": 6, "positive": 131 },
    "negative_rate_pct": 6.8,
    "positive_rate_pct": 11.9,
    "repair_rate_pct": 38.1
  },
  "signal_breakdown": [
    { "family": "confusion", "signal": "confusion", "count": 22 },
    { "family": "positive", "signal": "gratitude", "count": 87 }
  ],
  "detections": [
    {
      "detection_id": "0a6f4c1e-2b7d-4e83-9c50-1f2a3b4c5d6e",
      "conversation_id": "conv_5d1a7be2c90f4438",
      "user_id": "user_4821",
      "family": "confusion",
      "is_detected": true,
      "score": 0.74,
      "risk_score": 0.55,
      "trajectory": "sustained",
      "elicitor": "bot",
      "primary_cause": "Agent used internal integration names the user did not recognize.",
      "evidence": "User asked what a 'connector token' is twice; the agent did not rephrase.",
      "user_quotes": [
        { "turn": 9, "quote": "Sorry, what is a connector token? Where do I find that?", "signal": "confusion" }
      ],
      "expressed_signals": ["confusion"],
      "observed_signals": [],
      "key_turns": [9],
      "downgrade_reason": "",
      "detection_grade": "rlm",
      "message_count": 31,
      "detected_at": "2026-08-13T18:24:10",
      "conversation_context": null
    }
  ],
  "pagination": { "limit": 1, "offset": 0, "total": 22, "has_more": true }
}
```

## GET /tool-failures

Per-tool failure KPIs for the window. Only tools with at least one failure are listed. This endpoint has no pagination; use [`/tool-failures/:toolName`](#get-tool-failurestoolname) for examples.

### Query parameters

<ParamField query="days_back" type="integer" default="7">
  1–90.
</ParamField>

### Response fields

<ResponseField name="summary" type="object">
  `total` (failed calls), `total_calls` (all calls), `conversations` (with a failure), `tools` (with a failure), `failure_rate_pct`.
</ResponseField>

<ResponseField name="tools" type="array">
  Ordered by `failure_count` descending. Each has `tool_name`, `failure_count`, `total_count`, `failure_rate_pct`, `conversation_count`, `top_error` (a sample error message, truncated to 80 characters), `last_seen`.
</ResponseField>

### Example

```bash theme={"dark"}
curl "https://moda.dev/api/v1/data/tool-failures?days_back=7" \
  -H "x-api-key: YOUR_MODA_API_KEY"
```

```json theme={"dark"}
{
  "summary": { "total": 67, "total_calls": 5204, "conversations": 51, "tools": 6, "failure_rate_pct": 1.3 },
  "tools": [
    {
      "tool_name": "create_ticket",
      "failure_count": 24,
      "total_count": 312,
      "failure_rate_pct": 7.7,
      "conversation_count": 19,
      "top_error": "422 Unprocessable Entity: field 'priority' must be one of low, normal, high",
      "last_seen": "2026-08-14T11:02:36"
    },
    {
      "tool_name": "calendar_connect",
      "failure_count": 17,
      "total_count": 96,
      "failure_rate_pct": 17.7,
      "conversation_count": 14,
      "top_error": "OAuth exchange failed: invalid_grant",
      "last_seen": "2026-08-14T10:48:01"
    }
  ]
}
```

## GET /tool-failures/:toolName

Failure detail for one tool: a breakdown by error subtype plus individual examples with embedded conversation context. Subtype names come from your tenant's failure taxonomy — they are labels, not a fixed enum.

### Query parameters

<ParamField query="subtype" type="string">
  Filter `examples` (and the pagination `total`) to one error subtype. The `subtypes` breakdown is always unfiltered.
</ParamField>

<ParamField query="days_back" type="integer" default="7">
  1–90.
</ParamField>

<ParamField query="limit" type="integer" default="5">
  1–20.
</ParamField>

<ParamField query="offset" type="integer" default="0">
  0–10,000.
</ParamField>

### Response fields

<ResponseField name="tool_name" type="string" />

<ResponseField name="subtypes" type="array">
  `{subtype, count, conversation_count, sample_error, last_seen}` per error subtype.
</ResponseField>

<ResponseField name="examples" type="array">
  Newest first. Each has `failure_id`, `conversation_id`, `error_message`, `error_subtype`, `tool_input` (truncated), `msg_index`, `detected_at`, and `conversation` — an embedded context window centered on the failing call, or `null`.
</ResponseField>

<ResponseField name="pagination" type="object">
  `limit`, `offset`, `total`, `has_more`.
</ResponseField>

### Example

```bash theme={"dark"}
curl "https://moda.dev/api/v1/data/tool-failures/create_ticket?days_back=7&limit=1" \
  -H "x-api-key: YOUR_MODA_API_KEY"
```

```json theme={"dark"}
{
  "tool_name": "create_ticket",
  "subtypes": [
    { "subtype": "validation_error", "count": 18, "conversation_count": 14, "sample_error": "422 Unprocessable Entity: field 'priority' must be one of low, normal, high", "last_seen": "2026-08-14T11:02:36" },
    { "subtype": "timeout", "count": 6, "conversation_count": 6, "sample_error": "Request timed out after 30000ms", "last_seen": "2026-08-13T22:14:52" }
  ],
  "examples": [
    {
      "failure_id": "8c1d2e3f-4a5b-6c7d-8e9f-0a1b2c3d4e5f",
      "conversation_id": "conv_9f2c41d08ab37e51",
      "error_message": "422 Unprocessable Entity: field 'priority' must be one of low, normal, high",
      "error_subtype": "validation_error",
      "tool_input": "{\"topic\":\"refund\",\"plan\":\"annual\",\"priority\":\"urgent\"}",
      "msg_index": 7,
      "detected_at": "2026-08-14T09:31:41",
      "conversation": {
        "conversation_id": "conv_9f2c41d08ab37e51",
        "total_messages": 18,
        "summary": "User asks how to request a refund for an annual plan; agent explains the policy and opens a ticket.",
        "context": {
          "center_index": 7,
          "from_index": 5,
          "to_index": 9,
          "messages": [
            { "index": 7, "role": "assistant", "content": "", "tool_calls": [{ "id": "toolu_01Xk2m", "name": "create_ticket", "input": "{\"topic\":\"refund\",\"plan\":\"annual\",\"priority\":\"urgent\"}" }], "tool_results": [], "timestamp": "2026-08-14T09:31:40" }
          ]
        }
      }
    }
  ],
  "pagination": { "limit": 1, "offset": 0, "total": 24, "has_more": true }
}
```

## GET /hallucinations

Grounding results: agent claims checked against the conversation's world state and tool results.

<Note>
  The per-message `detections` list contains only two kinds of rows: `contradicted` (a claim contradicted by evidence) and `verified` (a claim confirmed by evidence). Ungrounded claims — statements with no supporting or contradicting evidence — appear only in the `summary` aggregates and are never listed per message.
</Note>

### Query parameters

<ParamField query="days_back" type="integer" default="7">
  1–90.
</ParamField>

<ParamField query="limit" type="integer" default="10">
  1–20.
</ParamField>

<ParamField query="offset" type="integer" default="0">
  0–10,000.
</ParamField>

<ParamField query="conversation_id" type="string">
  Scope the response — including the `summary` aggregates — to one conversation.
</ParamField>

<ParamField query="kind" type="string">
  `contradicted` or `verified`. Filters the `detections` list and its `total`.
</ParamField>

### Response fields

<ResponseField name="summary" type="object">
  Counts across all scored claims: `total_scored`, `contradicted_count`, `verified_count`, `ungrounded_count`, `ungrounded_rate` (0–1), `conversations_scored`, `conversations_with_contradiction`, and `rule_breakdown[]` (`{source, rule_id, count}` for contradictions).
</ResponseField>

<ResponseField name="detections" type="array">
  Contradicted rows first, then by confidence. Each has `conversation_id`, `message_index`, `unit_id`, `segment_id`, `kind` (`contradicted` or `verified`), `label`, `confidence`, `source`, `rule_id`, `model_id`, `violated_receipt_id` (or `null`), `violated_slot_key`, `violated_thread_id`, `offending_substring`, `evidence_quote`, `cws_high_water_msg_index`, `message_count`, `detected_at`.
</ResponseField>

<ResponseField name="pagination" type="object">
  `limit`, `offset`, `total`. This endpoint does not return `has_more`; page until fewer than `limit` rows come back or `offset + limit >= total`.
</ResponseField>

### Example

```bash theme={"dark"}
curl "https://moda.dev/api/v1/data/hallucinations?days_back=7&kind=contradicted&limit=1" \
  -H "x-api-key: YOUR_MODA_API_KEY"
```

```json theme={"dark"}
{
  "summary": {
    "total_scored": 412,
    "contradicted_count": 9,
    "verified_count": 361,
    "ungrounded_count": 42,
    "ungrounded_rate": 0.102,
    "conversations_scored": 288,
    "conversations_with_contradiction": 8,
    "rule_breakdown": [
      { "source": "rule", "rule_id": "success_vs_error_receipt", "count": 6 },
      { "source": "judge", "rule_id": "", "count": 3 }
    ]
  },
  "detections": [
    {
      "conversation_id": "conv_9f2c41d08ab37e51",
      "message_index": 8,
      "unit_id": "f4a7c09d81e5b3628c1d5a9e0b7f43216d8e2a5c9b0f7e341a6d8c2b5e9f0a73",
      "segment_id": "c7e5a1d2-4f6b-5a38-9c21-8b0e3d47f6a9",
      "kind": "contradicted",
      "label": "CONTRADICT",
      "confidence": 0.97,
      "source": "rule",
      "rule_id": "success_vs_error_receipt",
      "model_id": "",
      "violated_receipt_id": "5b8d2f61-3c9a-4e07-b4d8-6f1a2c3e5d90",
      "violated_slot_key": "",
      "violated_thread_id": "refund_ticket",
      "offending_substring": "I've created the ticket for you",
      "evidence_quote": "create_ticket failed: 422 Unprocessable Entity",
      "cws_high_water_msg_index": 8,
      "message_count": 18,
      "detected_at": "2026-08-14T09:47:19"
    }
  ],
  "pagination": { "limit": 1, "offset": 0, "total": 9 }
}
```

## Next steps

* [Conversations](/data-api/conversations) — pull a wider context window around any detection's key turn.
* [Problems](/data-api/problems) — signals grouped into ranked root-cause problems.
* [Dashboard: Signals](/dashboard/signals) — the same detections with the full emotion and failure taxonomies.
