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

# Reports

> Pull report numbers, trends, and breakdowns from the same metrics as the dashboard Reports page.

Reports return numbers from a fixed catalog of metrics, such as conversations, AI-resolved conversations, task closes, and retention offers. You pick up to five metrics and a time range, and optionally a breakdown by week, phone line, team member, and more.

`POST /reports` only reads data. It uses `POST` because the request is a JSON body.

## Find the metric for your question

| Question | Metrics | Useful `group_by` |
| - | - | - |
| How fast does someone act on a new request? (speed to lead) | `median_hours_to_first_action`, `avg_hours_to_first_action` | `team_member`, `category` |
| How long does it take to close a request? | `median_hours_to_close`, `avg_hours_to_close` (`team_member` or `category`), `same_day_close_rate` (`team_member` only) | `team_member`, `category` |
| How much does the AI handle without a person? | `ai_resolved_rate`, `ai_resolved_conversations`, `caller_resolved_rate` | `week`, `phone_line` for the counts |
| What is waiting on someone right now? | `open_tasks` | `task_age`, `team_member` |
| Who on the team does the most work? | `task_closes`, `tasks_worked` | `team_member` |
| How often does a customer ask for a person, and how often does the AI resolve it anyway? | `asked_for_person_conversations`, `asked_for_person_ai_resolved_rate` | `phone_line` for the count |
| How do retention offers do? | `retention_offers_pitched`, `retention_offers_accepted`, `retention_acceptance_rate`, `memberships_saved` | None. Run one report per period or per `filters.phone_line_id`. |
| How does each location compare? | Most conversation counts | `phone_line` |

Read [these definitions](#read-these-definitions-first) before you put a number in front of anyone. AI-resolved, for example, does not mean the customer's problem was solved.

<Warning>
  **HTTP `200` does not mean the report ran.** A combination of metrics, grouping, filters, and range that the catalog does not support returns HTTP `200` with `"status": "unsupported_combination"` and no numbers. Always check `status` before you read `values`. See [Unsupported requests](#unsupported-requests).
</Warning>

## Run a report

```
POST /reports
```

[Try it in the playground](/api-reference/reports/run-a-report).

<CodeGroup>
  ```bash cURL theme={null}
  curl https://answeringagent.com/api/v1/reports \
    -H "X-API-KEY: $ANSWERING_AGENT_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{ "measures": ["conversations"], "range": "30d", "group_by": "phone_line" }'
  ```

  ```javascript JavaScript theme={null}
  const res = await fetch('https://answeringagent.com/api/v1/reports', {
    method: 'POST',
    headers: {
      'X-API-KEY': process.env.ANSWERING_AGENT_API_KEY,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({ measures: ['conversations'], range: '30d', group_by: 'phone_line' }),
  });
  if (!res.ok) throw new Error(`${res.status}: ${await res.text()}`);
  const report = await res.json();
  // HTTP 200 does not mean the report ran. Check status first.
  if (report.status !== 'ok') throw new Error(`${report.status}: ${report.message}`);
  ```
</CodeGroup>

### Request body

<ParamField body="measures" type="string[]" required>
  One to five metric keys from the [catalog](#metric-catalog). Each key once.
</ParamField>

<ParamField body="range" type="string" required>
  The time window, in your team's timezone:

  * `today`, `yesterday`, `7d`, `14d`, `30d`, `90d`, `365d`, `mtd`, or `ytd`.
  * `month`, with `month` set to a calendar month.
  * `custom`, with `from` and `to` set. Up to 366 days.

  `today` counts. `30d` starts 30 days ago at the current time of day in your team's timezone, and ends now. On October 6, the response's `range` shows `from` `2026-09-06` and `to` `2026-10-06`: 31 dates, the first and last of them partial. The other day ranges work the same way. For whole days, use `custom`.
</ParamField>

<ParamField body="month" type="string">
  `YYYY-MM`. Required when `range` is `month`.
</ParamField>

<ParamField body="from" type="string">
  `YYYY-MM-DD`, the first day. Required when `range` is `custom`.
</ParamField>

<ParamField body="to" type="string">
  `YYYY-MM-DD`, the last day, included. Required when `range` is `custom`.
</ParamField>

<ParamField body="group_by" type="string">
  Break the numbers into rows. One of `day`, `week`, `month`, `hour_of_day`, `category`, `channel`, `phone_line`, `team_member`, `task_age`, `action_group`, `failure_type`, `failure_subcategory`, or `cancellation_reason`. Each metric supports only some of them. See the [catalog](#metric-catalog).

  * `hour_of_day` returns 24 rows, labeled `00:00` to `23:00`, by the hour each conversation started in your team's timezone. It works with `include_percent_of_total` but not with `top_n`.
  * `task_age` works only with `open_tasks`. It returns four rows by the time since the conversation or form came in: `Under 1 day`, `1 to 3 days`, `3 to 7 days`, and `7 days or more`.
</ParamField>

<ParamField body="split_by" type="string">
  `team_member`. Splits one team-work metric (`tasks_worked` or `task_closes`) by team member over time. Requires `group_by` of `day` or `week`.
</ParamField>

<ParamField body="filters" type="object">
  Narrow the data.

  <Expandable title="filters">
    <ParamField body="channel" type="string">
      `phone`, `text`, `web_chat`, or `email`, the same values as conversation search.
    </ParamField>

    <ParamField body="phone_line_id" type="integer">
      One phone line. Get IDs from [`GET /account`](/api/account).
    </ParamField>

    <ParamField body="category" type="string">
      A category key: `damage_claim`, `cancellation`, `lost_items`, `sales`, `scheduling`, `hours_status`, `service_status`, `billing`, `account_update`, `general_questions`, `job_seeking`, `no_answer`, `not_stated`, `spam`, or `other`. Task and team-work metrics also accept `form`. [`GET /reports/metrics`](#list-the-metrics) lists each key's label under `categories`.
    </ParamField>

    <ParamField body="language" type="string">
      A two-letter language code, such as `es`.
    </ParamField>

    <ParamField body="team_member_ids" type="integer[]">
      One to five team member IDs from `members[].id` in [`GET /account`](/api/account), each once. Team-work metrics only.
    </ParamField>
  </Expandable>
</ParamField>

<ParamField body="top_n" type="integer">
  1 to 10. Keep only the largest rows of a category-style breakdown, such as `category`, `team_member`, or `cancellation_reason`. For `category`, the rest go into one `All other categories` row. Any other grouping returns `unsupported_combination`. Without `top_n`, `team_member_activity` and `account_updates` return their top 5 rows.
</ParamField>

<ParamField body="include_percent_of_total" type="boolean">
  Add `percent_of_total` to each row of a category-style or channel breakdown. One grouped count metric at a time.
</ParamField>

### Response

```json theme={null}
{
  "status": "ok",
  "range": { "requested": "30d", "from": "2026-09-06", "to": "2026-10-06", "timezone": "America/Chicago" },
  "values": {
    "conversations": { "label": "Conversations", "unit": "count", "value": 1264 }
  },
  "rows": [
    {
      "label": "Lamar Blvd",
      "phone_line": { "id": 2182, "name": "Lamar Blvd", "phone_number": "+15125550177" },
      "values": { "conversations": 461 }
    },
    {
      "label": "Westside Wash",
      "phone_line": { "id": 2181, "name": "Westside Wash", "phone_number": "+15125550142" },
      "values": { "conversations": 803 }
    }
  ],
  "resolved_group_by": "phone_line",
  "chart": {
    "kind": "bar",
    "title": "Conversations (2026-09-06 to 2026-10-06)",
    "label_kind": "phone_line",
    "series": [
      { "name": "Conversations", "points": [{ "label": "Lamar Blvd", "value": 461 }, { "label": "Westside Wash", "value": 803 }] }
    ]
  },
  "methodology": [
    "Counts each conversation shown on the dashboard, so spam and unanswered calls are left out. When a text or email continues a phone call conversation, it counts under both channels, so channel rows can add up to more than the total."
  ],
  "caveats": [],
  "url": "https://answeringagent.com/dashboard/reports"
}
```

<ResponseField name="status" type="string">
  `ok` when the report ran. See [Unsupported requests](#unsupported-requests) for the other values.
</ResponseField>

<ResponseField name="range" type="object">
  The window the report used: the `requested` range, the `from` and `to` dates (both included), and the team `timezone`.
</ResponseField>

<ResponseField name="values" type="object">
  One entry for each requested metric, keyed by metric. Each entry has a `label`, a `unit` (`count`, `percent`, `minutes`, `hours`, or `score`), and a `value`, which is `null` when there is no data. Rates also include `numerator` and `denominator`. A count with a natural base, such as `memberships_saved`, also has `numerator`, `denominator`, and `rate`. A percent `value` or `rate` runs from 0 to 100, rounded to one decimal place.
</ResponseField>

<ResponseField name="rows" type="object[]">
  The breakdown, when there is one. Each row has a `label` and `values`, usually keyed by metric. Only metrics that support the grouping appear in `values`. A metric with a `null` value has no data for that row.

  A row grouped by `channel`, `category`, `phone_line`, or `team_member` also has that key, next to `label`:

  * `channel`: a channel, such as `phone`.
  * `category`: a category key, such as `cancellation`. `null` on the `Uncategorized` and `All other categories` rows.
  * `phone_line`: the line's `id`, `name`, and `phone_number`.
  * `team_member`: the person's `id` and `name`. `null` on the `Unassigned` row.

  Use these keys to match rows to your own records. The `label` is for display.

  A few breakdowns use other keys in `values`. `team_member_activity` uses `tasks_completed`, `messages_sent`, and `notes_added`. `account_updates` uses `account_updates`, `completed`, and `failures`. `conversation_categories` by `phone_line` uses `Total` plus one key for each category label. `split_by` uses team member names.

  Count metrics that support `day` get a trend in `rows` even without `group_by`: daily for ranges up to 31 days, weekly beyond that. Ranges `today` and `yesterday` get no trend.
</ResponseField>

<ResponseField name="resolved_group_by" type="string">
  The grouping the rows use. It can differ from your `group_by`. A daily series longer than 62 days comes back weekly.
</ResponseField>

<ResponseField name="chart" type="object">
  The chart the dashboard draws from the rows: `kind` (`line` or `bar`), `title`, `label_kind`, and up to five `series` of `points`. Absent when the rows cannot be charted.
</ResponseField>

<ResponseField name="methodology" type="string[]">
  How each metric is counted. Read these before you compare numbers with another system.
</ResponseField>

<ResponseField name="caveats" type="string[]">
  Anything that limits this result, such as missing history or overlapping rows.
</ResponseField>

<ResponseField name="url" type="string">
  The dashboard Reports page.
</ResponseField>

### Unsupported requests

The catalog limits which metrics, groupings, and filters go together. A request outside those limits returns `status` set to `unsupported_combination`, with a `reason`, a `message`, and the `allowed_alternatives` you can retry with.

```json theme={null}
{
  "status": "unsupported_combination",
  "error": "unsupported_combination",
  "reason": "unsupported_group_by",
  "message": "None of the selected measures supports that grouping.",
  "allowed_alternatives": { "group_by": [] },
  "url": "https://answeringagent.com/dashboard/reports"
}
```

An ID in `filters.team_member_ids` that is not a current team member returns `reason` set to `unknown_team_member`, with a `message` that lists the IDs.

These responses come back with HTTP `200`, not a `4xx`. A client that checks only the HTTP status treats them as success. Check `status` before you read `values`. A missing `month`, `from`, or `to`, or a custom range that is reversed or longer than 366 days, is also an `unsupported_combination`. A malformed body returns `400`. That includes an unknown metric key and any field the API does not know, such as `grouping` or `filters.phone_number_id`. A `filters.phone_line_id` that is not one of the key owner's phone lines returns `404` with `code` `not_found`.

## Read these definitions first

Several metrics mean something narrower than their name. The `methodology` in each response says exactly what was counted.

* **AI-resolved** (`ai_resolved_conversations`) means nobody on your team replied, changed the task, or added a note. It does not mean the customer's problem was solved. For that, use `caller_resolved_rate`.
* **Meaningful conversations** (`meaningful_conversations`) have enough real exchange to judge how they went: phone calls longer than 10 seconds with a transcript where the caller said enough, and website chats with a real exchange or a follow-up task. Texts and emails are never counted.
* **Handled** (`handled_conversations`) means a meaningful conversation that left no task for your team. **Needs team** (`needs_team_conversations`) means a meaningful conversation that did leave one. Handled does not mean the customer's problem was solved.
* **Hours to close and hours to first action** count elapsed hours from when the conversation or form came in. They are not business hours or time spent working.
* **First action** is the first time a person changed a task's status or assignee, added a note, sent a reply, or closed it. Creating the task does not count. Because assignment and notes count, hours to first action is not a customer response time. Tasks nobody has acted on are left out of the hours-to-first-action metrics. To see those, run `open_tasks` with `group_by` `task_age`.
* **Asked for a person, resolved by AI** (`asked_for_person_ai_resolved_conversations`) counts conversations where the customer asked for a person, the caller's need was met, nobody on your team worked it, and the AI attempted no transfer.
* **Transfer attempts** (`transfer_attempts`) count conversations where the AI tried to transfer the caller. A transfer attempt does not mean a person answered.
* **Tasks completed** (`tasks_completed`) counts tasks marked done, including tasks the AI closed on its own. To count closes by people, use `task_closes`.
* **Team-work metrics** (`tasks_worked`, `task_closes`, the hours-to-close and hours-to-first-action metrics, `same_day_close_rate`, and `open_tasks`) have data from July 25, 2026. A range that ends before then returns `unsupported_combination`.
* **Memberships saved** (`memberships_saved`) counts only cancellation conversations where the AI pitched a retention offer and the membership was saved. A save with no retention offer pitched is not counted. It counts from July 1, 2026, when save tracking started. Its `denominator` is the cancellation conversations with a retention offer pitched in the same period.
* **Open tasks right now** (`open_tasks`) is a snapshot. It requires `range` `today`.

## Examples

### AI-resolved rate by week

`ai_resolved_rate` has no weekly breakdown. Request the two counts by week and divide them. The headline rate for the whole range comes back in `values`.

```json theme={null}
{
  "measures": ["conversations", "ai_resolved_conversations", "ai_resolved_rate"],
  "range": "custom",
  "from": "2026-09-08",
  "to": "2026-10-05",
  "group_by": "week"
}
```

```json theme={null}
{
  "status": "ok",
  "range": { "requested": "custom", "from": "2026-09-08", "to": "2026-10-05", "timezone": "America/Chicago" },
  "values": {
    "conversations": { "label": "Conversations", "unit": "count", "value": 1184 },
    "ai_resolved_conversations": { "label": "AI-resolved conversations", "unit": "count", "value": 497 },
    "ai_resolved_rate": { "label": "AI-resolved rate", "unit": "percent", "value": 42, "numerator": 497, "denominator": 1184 }
  },
  "rows": [
    { "label": "2026-09-08", "values": { "conversations": 291, "ai_resolved_conversations": 118 } },
    { "label": "2026-09-15", "values": { "conversations": 302, "ai_resolved_conversations": 126 } },
    { "label": "2026-09-22", "values": { "conversations": 288, "ai_resolved_conversations": 121 } },
    { "label": "2026-09-29", "values": { "conversations": 303, "ai_resolved_conversations": 132 } }
  ],
  "resolved_group_by": "week",
  "methodology": [
    "Counts each conversation shown on the dashboard, so spam and unanswered calls are left out. When a text or email continues a phone call conversation, it counts under both channels, so channel rows can add up to more than the total.",
    "Counts conversations the AI finished where nobody on your team replied, changed the task, or added a note. This measures that no person had to step in, not that the caller got what they needed. Caller resolved rate measures that.",
    "AI-resolved conversations divided by all conversations. AI-resolved means no person had to step in, not that the caller got what they needed."
  ],
  "caveats": [],
  "url": "https://answeringagent.com/dashboard/reports"
}
```

The weekly rate for the week of September 8 is 118 ÷ 291, or 40.5%. Each week label is the first day of a seven-day bucket. Buckets count back from the last day of the range, so pick a range whose length is a multiple of seven to get full weeks. The `chart` field is left out of this example and the ones below.

### Tasks closed per team member, with median hours to close

```json theme={null}
{
  "measures": ["task_closes", "median_hours_to_close"],
  "range": "30d",
  "group_by": "team_member"
}
```

```json theme={null}
{
  "status": "ok",
  "range": { "requested": "30d", "from": "2026-09-06", "to": "2026-10-06", "timezone": "America/Chicago" },
  "values": {
    "task_closes": { "label": "Task closes", "unit": "count", "value": 212 },
    "median_hours_to_close": { "label": "Median hours to close", "unit": "hours", "value": 5.2 }
  },
  "rows": [
    {
      "label": "Priya Shah",
      "team_member": { "id": 9034, "name": "Priya Shah" },
      "values": { "task_closes": 96, "median_hours_to_close": 3.1 }
    },
    {
      "label": "Sam Ortiz",
      "team_member": { "id": 9021, "name": "Sam Ortiz" },
      "values": { "task_closes": 71, "median_hours_to_close": 6.4 }
    },
    {
      "label": "Jordan Lee",
      "team_member": { "id": 9040, "name": "Jordan Lee" },
      "values": { "task_closes": 45, "median_hours_to_close": 9.8 }
    }
  ],
  "resolved_group_by": "team_member",
  "methodology": [
    "Counts each time a person marked a task done. A task reopened and closed again counts twice; tasks closed automatically are left out.",
    "Each time a person closed a task, takes the hours since its conversation or form came in, then reports the middle value. This is elapsed time, not active handling time. Category rows use the task’s current category, not the category it had when it was closed."
  ],
  "caveats": [],
  "url": "https://answeringagent.com/dashboard/reports"
}
```

Rows are sorted by the first grouped metric, largest first. Hours to close run from when the conversation or form came in. They measure elapsed time, not time spent working.

### Retention offers pitched and accepted

```json theme={null}
{
  "measures": [
    "cancellation_conversations",
    "retention_offers_pitched",
    "retention_offers_accepted",
    "retention_acceptance_rate",
    "memberships_saved"
  ],
  "range": "month",
  "month": "2026-09"
}
```

```json theme={null}
{
  "status": "ok",
  "range": { "requested": "month", "from": "2026-09-01", "to": "2026-09-30", "timezone": "America/Chicago" },
  "values": {
    "cancellation_conversations": { "label": "Cancellation conversations", "unit": "count", "value": 64 },
    "retention_offers_pitched": { "label": "Retention offers pitched", "unit": "count", "value": 41 },
    "retention_offers_accepted": { "label": "Retention offers accepted", "unit": "count", "value": 9 },
    "retention_acceptance_rate": { "label": "Retention acceptance rate", "unit": "percent", "value": 22, "numerator": 9, "denominator": 41 },
    "memberships_saved": { "label": "Memberships saved", "unit": "count", "value": 7, "numerator": 7, "denominator": 41, "rate": 17.1 }
  },
  "methodology": [
    "Counts conversations in the Cancellation category.",
    "Counts cancellation conversations where the AI made an offer to keep the customer.",
    "Counts cancellation conversations where the customer accepted the offer the AI made to keep them.",
    "Retention offers accepted divided by retention offers pitched, on cancellation conversations only.",
    "Counts memberships confirmed as saved, starting 2026-07-01 when save tracking began. Earlier conversations have no save record. The save rate divides saves by retention offers pitched from 2026-07-01 on, so its base is only the offers made since tracking began."
  ],
  "caveats": [],
  "url": "https://answeringagent.com/dashboard/reports"
}
```

None of these metrics has a daily breakdown, so the response has no `rows`. For one location, add `"filters": { "phone_line_id": 2181 }`. To see why customers cancel, run `cancellation_reasons` with `group_by` set to `cancellation_reason`.

### Conversations by phone line

```json theme={null}
{
  "measures": ["conversations", "ai_resolved_conversations"],
  "range": "30d",
  "group_by": "phone_line",
  "filters": { "channel": "phone" }
}
```

```json theme={null}
{
  "status": "ok",
  "range": { "requested": "30d", "from": "2026-09-06", "to": "2026-10-06", "timezone": "America/Chicago" },
  "values": {
    "conversations": { "label": "Conversations", "unit": "count", "value": 1032 },
    "ai_resolved_conversations": { "label": "AI-resolved conversations", "unit": "count", "value": 431 }
  },
  "rows": [
    {
      "label": "Lamar Blvd",
      "phone_line": { "id": 2182, "name": "Lamar Blvd", "phone_number": "+15125550177" },
      "values": { "conversations": 377, "ai_resolved_conversations": 143 }
    },
    {
      "label": "Westside Wash",
      "phone_line": { "id": 2181, "name": "Westside Wash", "phone_number": "+15125550142" },
      "values": { "conversations": 655, "ai_resolved_conversations": 288 }
    }
  ],
  "resolved_group_by": "phone_line",
  "methodology": [
    "Counts each conversation shown on the dashboard, so spam and unanswered calls are left out. When a text or email continues a phone call conversation, it counts under both channels, so channel rows can add up to more than the total.",
    "Counts conversations the AI finished where nobody on your team replied, changed the task, or added a note. This measures that no person had to step in, not that the caller got what they needed. Caller resolved rate measures that."
  ],
  "caveats": [],
  "url": "https://answeringagent.com/dashboard/reports"
}
```

The response has one row for each phone line the key's owner can see, sorted by name. Match rows by `phone_line.id`. The `label` is the line name, and when two lines share a name it adds the phone number. The `phone` channel filter limits the report to calls. Leave it out to count texts, chats, and emails too. For a metric that does not group by phone line, run one report per line with `filters.phone_line_id`.

### Calls per hour

To find your busiest hours, group `conversations` by `hour_of_day` and filter to phone calls.

```json theme={null}
{
  "measures": ["conversations"],
  "range": "30d",
  "group_by": "hour_of_day",
  "filters": { "channel": "phone" }
}
```

```json theme={null}
{
  "status": "ok",
  "range": { "requested": "30d", "from": "2026-09-06", "to": "2026-10-06", "timezone": "America/Chicago" },
  "values": {
    "conversations": { "label": "Conversations", "unit": "count", "value": 1032 }
  },
  "rows": [
    { "label": "00:00", "values": { "conversations": 2 } },
    { "label": "01:00", "values": { "conversations": 1 } },
    { "label": "02:00", "values": { "conversations": 1 } },
    { "label": "03:00", "values": { "conversations": 0 } },
    { "label": "04:00", "values": { "conversations": 1 } },
    { "label": "05:00", "values": { "conversations": 3 } },
    { "label": "06:00", "values": { "conversations": 11 } },
    { "label": "07:00", "values": { "conversations": 37 } },
    { "label": "08:00", "values": { "conversations": 70 } },
    { "label": "09:00", "values": { "conversations": 87 } },
    { "label": "10:00", "values": { "conversations": 91 } },
    { "label": "11:00", "values": { "conversations": 85 } },
    { "label": "12:00", "values": { "conversations": 88 } },
    { "label": "13:00", "values": { "conversations": 93 } },
    { "label": "14:00", "values": { "conversations": 87 } },
    { "label": "15:00", "values": { "conversations": 81 } },
    { "label": "16:00", "values": { "conversations": 78 } },
    { "label": "17:00", "values": { "conversations": 72 } },
    { "label": "18:00", "values": { "conversations": 60 } },
    { "label": "19:00", "values": { "conversations": 41 } },
    { "label": "20:00", "values": { "conversations": 24 } },
    { "label": "21:00", "values": { "conversations": 11 } },
    { "label": "22:00", "values": { "conversations": 5 } },
    { "label": "23:00", "values": { "conversations": 3 } }
  ],
  "resolved_group_by": "hour_of_day",
  "methodology": [
    "Counts each conversation shown on the dashboard, so spam and unanswered calls are left out. When a text or email continues a phone call conversation, it counts under both channels, so channel rows can add up to more than the total."
  ],
  "caveats": [],
  "url": "https://answeringagent.com/dashboard/reports"
}
```

Each row counts the calls that started in that hour, in the team's timezone, across all 30 days. The busiest hour here is 13:00, with 93 calls. For a daily average, divide by the number of days: 93 ÷ 30 is about 3 calls between 1 and 2 p.m. on a typical day. Add `"include_percent_of_total": true` to get each hour's share of the day's calls.

## List the metrics

```
GET /reports/metrics
```

[Try it in the playground](/api-reference/reports/list-report-metrics).

Returns the full catalog. Read it from code instead of hard-coding the table below. New metrics appear there first.

<CodeGroup>
  ```bash cURL theme={null}
  curl https://answeringagent.com/api/v1/reports/metrics \
    -H "X-API-KEY: $ANSWERING_AGENT_API_KEY"
  ```

  ```javascript JavaScript theme={null}
  const res = await fetch('https://answeringagent.com/api/v1/reports/metrics', {
    headers: { 'X-API-KEY': process.env.ANSWERING_AGENT_API_KEY },
  });
  if (!res.ok) throw new Error(`${res.status}: ${await res.text()}`);
  const { measures } = await res.json();
  ```
</CodeGroup>

```json theme={null}
{
  "measures": [
    {
      "key": "conversation_categories",
      "label": "Conversation categories",
      "description": "Conversation counts for every category, optionally for each phone line.",
      "unit": "count",
      "group_by": ["category", "phone_line"],
      "requires_group_by": true,
      "split_by": [],
      "methodology": "Groups conversations that have a category by that category. Percentages use all conversations with a category as the base."
    }
  ],
  "ranges": ["today", "yesterday", "7d", "14d", "30d", "90d", "365d", "mtd", "ytd", "month", "custom"],
  "group_by": ["day", "week", "month", "hour_of_day", "category", "channel", "phone_line", "team_member", "..."],
  "split_by": ["team_member"],
  "categories": [
    { "key": "damage_claim", "label": "Damage Claim" },
    { "key": "cancellation", "label": "Cancellation" },
    { "key": "form", "label": "Form" }
  ],
  "filters": {
    "channel": { "type": "string", "enum": ["phone", "text", "web_chat", "email"] },
    "team_member_ids": { "type": "array", "minItems": 1, "maxItems": 5, "items": { "type": "integer", "exclusiveMinimum": 0 } }
  }
}
```

When `requires_group_by` is `true`, the metric fails without one of its `group_by` values. `categories` lists every category key with its label. `filters` maps each filter name to its JSON Schema. The example shortens `measures`, `categories`, and `filters`.

## Metric catalog

Fifty-four metrics as of October 2026. The table is generated from the same code as [`GET /reports/metrics`](#list-the-metrics). A metric with no `group_by` returns only a total. "Required" means the metric needs one of the listed groupings. [Read these definitions first](#read-these-definitions-first) for the terms in the Description column.

### Conversations and calls

| Key | Label | Description | Unit | `group_by` |
| - | - | - | - | - |
| `conversations` | Conversations | Every conversation shown on the dashboard: phone calls, texts, website chats, and emails. | count | `day`, `week`, `month`, `channel`, `phone_line`, `hour_of_day` |
| `ai_resolved_conversations` | AI-resolved conversations | Conversations the AI finished without anyone on your team doing any work. | count | `day`, `week`, `channel`, `phone_line`, `hour_of_day` |
| `ai_resolved_rate` | AI-resolved rate | Share of conversations the AI finished without anyone on your team doing any work. | percent | None |
| `meaningful_conversations` | Meaningful conversations | Conversations with enough real exchange to judge how they went. Handled and needs-team conversations are counted out of these. | count | `day`, `week`, `channel`, `phone_line`, `hour_of_day` |
| `handled_conversations` | Handled conversations | Meaningful conversations that left no follow-up task for your team. | count | `day`, `week`, `channel`, `phone_line`, `hour_of_day` |
| `handled_rate` | Handled rate | Share of meaningful conversations that left no follow-up task for your team. | percent | None |
| `needs_team_conversations` | Needs-team conversations | Meaningful conversations that left a follow-up task for your team. | count | `day`, `week`, `channel`, `phone_line`, `hour_of_day` |
| `asked_for_person_conversations` | Asked for a person | Conversations where the customer asked to speak with a person. | count | `channel`, `phone_line`, `hour_of_day` |
| `asked_for_person_ai_resolved_conversations` | Asked for a person, resolved by AI | Conversations where the customer asked for a person but the AI resolved it anyway. | count | `channel`, `phone_line`, `hour_of_day` |
| `asked_for_person_ai_resolved_rate` | Asked for a person, AI-resolved rate | Share of conversations where the customer asked for a person that the AI resolved anyway. | percent | None |
| `transfer_attempts` | Transfer attempts | Conversations where the AI tried to transfer the caller to your team. | count | `channel`, `phone_line`, `hour_of_day` |
| `conversation_categories` | Conversation categories | Conversation counts for every category, optionally for each phone line. | count | Required: `category` or `phone_line` |
| `conversations_with_tasks` | Conversations with tasks | Conversations that left a follow-up task, by channel or over time. | count | `day`, `week`, `channel`, `hour_of_day` |
| `conversations_with_tasks_rate` | Conversations with tasks rate | Share of conversations that left a follow-up task. | percent | None |
| `texts_sent` | Texts sent | Text messages sent from conversations that the text provider accepted. | count | None |
| `avg_duration_minutes` | Average duration | Average conversation length in minutes. | minutes | None |
| `account_updates` | Account updates | Changes the AI made to customer accounts, grouped by type of change. | count | Required: `action_group` |

### Call quality (phone calls only)

| Key | Label | Description | Unit | `group_by` |
| - | - | - | - | - |
| `handled_well_rate` | Handled well rate | Share of rated phone calls that the quality review marked handled well. | percent | None |
| `avg_quality_score` | Average quality score | Average score for how well the AI behaved on meaningful phone calls. | score | None |
| `quality_review_rate` | Quality review rate | Share of meaningful phone calls flagged for quality review. | percent | None |
| `caller_resolution_coverage_rate` | Caller resolution coverage | Share of meaningful phone calls with a recorded outcome for the caller. | percent | None |
| `caller_resolved_rate` | Caller resolved rate | Share of phone calls with a recorded outcome where the caller's need was fully met on the call. | percent | None |
| `caller_resolved_with_task_rate` | Caller resolved with a task rate | Share of phone calls where the caller's need was met that still left a follow-up task. | percent | None |
| `system_health_rate` | System health rate | Share of meaningful phone calls where your connected systems worked. | percent | None |
| `accuracy_rate` | Accuracy rate | Share of meaningful phone calls where the AI did not give wrong information. | percent | None |
| `escalation_failure_rate` | Escalation failure rate | Share of meaningful phone calls that should have been transferred to your team and were not. | percent | None |
| `quality_failure_types` | Quality failure types | Meaningful phone calls grouped by the main issue the quality review found. | count | Required: `failure_type` |
| `quality_knowledge_gaps` | Quality knowledge gaps | Phone calls where the AI could not help with what was asked, grouped by the reason. | count | Required: `failure_subcategory` |
| `questions_answered` | Questions answered | Questions callers asked on phone calls that the AI answered. | count | None |
| `questions_answered_rate` | Questions answered rate | Answered questions as a share of all questions callers asked on phone calls. | percent | None |

### Cancellations and offers

| Key | Label | Description | Unit | `group_by` |
| - | - | - | - | - |
| `cancellation_conversations` | Cancellation conversations | Conversations in the Cancellation category. | count | `month` |
| `cancellation_reasons` | Cancellation reasons | Why customers cancelled, from the reason recorded on each cancellation conversation. | count | Required: `cancellation_reason` |
| `retention_offers_pitched` | Retention offers pitched | Cancellation conversations where the AI made an offer to keep the customer. | count | None |
| `retention_offers_accepted` | Retention offers accepted | Cancellation conversations where the customer accepted the offer. | count | None |
| `retention_acceptance_rate` | Retention acceptance rate | Retention offers accepted as a share of retention offers pitched. | percent | None |
| `retention_discounts_applied` | Retention discounts applied | Accepted retention discounts that your connected business software confirmed it applied. | count | None |
| `memberships_saved` | Memberships saved | Cancellation conversations where the AI pitched a retention offer and the membership was saved, counted from 2026-07-01. | count | None |
| `offers_pitched` | Offers pitched | Offers the AI pitched across all conversations, in the greeting or during the conversation. | count | None |
| `offers_accepted` | Offers accepted | Offers customers accepted across all conversations, from the greeting or during the conversation. | count | None |
| `offer_acceptance_rate` | Offer acceptance rate | Offers accepted as a share of offers pitched. | percent | None |

### Tasks

| Key | Label | Description | Unit | `group_by` |
| - | - | - | - | - |
| `tasks_created` | Tasks created | Tasks created from conversations and forms that came in during the period. | count | None |
| `tasks_completed` | Tasks completed | Tasks from the period that are now marked done. | count | None |
| `tasks_still_open` | Tasks still open | Tasks from the period that are not marked done yet. | count | None |
| `task_completion_rate` | Task completion rate | Tasks completed as a share of tasks created. | percent | None |
| `task_categories` | Task categories | Task counts for every category. | count | Required: `category` |

### Team work

| Key | Label | Description | Unit | `group_by` |
| - | - | - | - | - |
| `tasks_worked` | Tasks worked | Distinct tasks a person worked on during the period. | count | `day`, `week`, `team_member` |
| `task_closes` | Task closes | Times a person marked a task done during the period. A reopened and reclosed task counts again. | count | `day`, `week`, `team_member` |
| `avg_hours_to_close` | Average hours to close | Average hours from when the conversation or form came in to when a person closed its task. | hours | `category`, `team_member` |
| `median_hours_to_close` | Median hours to close | Median hours from when the conversation or form came in to when a person closed its task. | hours | `category`, `team_member` |
| `avg_hours_to_first_action` | Average hours to first action | Average hours from when a conversation or form came in to the first time a person acted on its task. This is the speed-to-lead measure. | hours | `category`, `team_member` |
| `median_hours_to_first_action` | Median hours to first action | Median hours from when a conversation or form came in to the first time a person acted on its task. | hours | `category`, `team_member` |
| `same_day_close_rate` | Same-day close rate | Share of task closes by a person that happened on the same local day the request came in. | percent | `team_member` |
| `open_tasks` | Open tasks right now | Tasks not yet done right now, by current assignee (including unassigned tasks) or by how long ago they came in. | count | `team_member`, `task_age` |
| `team_member_activity` | Team member activity | Tasks completed, messages sent, and notes added by each team member. | count | Required: `team_member` |

`group_by` `month` covers at most 12 calendar months. With `team_member`, the hours-to-first-action metrics credit the person who acted first. For average call length, run `avg_duration_minutes` with `filters.channel` set to `phone`.


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