Skip to main content
Use the Chatarmin CX reporting API to track ticket volume, response times, satisfaction, and automation in your own dashboards.

Access and limits

Both endpoints extend the existing /public/v1 API. Send your API key in the cx-api-key header. The key needs analytics:read and must be assigned to a support agent. Configure it in Settings → API. Report queries enforce that agent’s dashboard permissions and channel, team, and agent restrictions. A metric appearing in the catalog does not grant permission to read its data. Neither endpoint changes tickets or sends messages. See rate limits for retry guidance and the OpenAPI reference for complete response schemas. For individual surveys and recorded login intervals rather than aggregates, use CSAT and session exports. Those endpoints require organization-wide dashboard access.

Discover metrics

The response is { "success": true, "data": [...] }. It contains the complete static catalog, without pagination. Each entry has: The catalog contains no ticket records, customer information, workspace entity IDs, or internal data-source names. Labels and descriptions are in English. Cache this metadata rather than fetching it before every query. Not every metric supports every grouping or visualization.

Query report data

Start with one metric and a short period. Send JSON with two required fields: query and an IANA timezone, such as Europe/Vienna or UTC. Raw pagination can add the optional cursor field described below.
The response uses { "success": true, "data": ... }. Inspect data.kind to interpret the result: for example, kpi contains value, previousValue, and format; time-series contains series and points. The request returns chart data, not an image or CSV export.

Query language

Write each clause on its own line. This is the CX report query language, not SQL: there are no arbitrary tables, joins, or SQL expressions. Public API v1 cuts over to the canonical forms on this page: WHERE, metric-free VISUALIZE TYPE, and typed SET clauses. The service can still read some older saved aliases for compatibility, but new clients must not generate FILTER, OPTIONS, or metric lists in VISUALIZE. Supported public v1 visualizations include kpi, line, area, grouped_bar, stacked_bar, split_bar, horizontal_bar, data_table, and agent_table. Charts require metrics with the same format; do not stack unlike units or non-additive series. A data_table can mix formats and supports up to two category groups plus one optional time grain. The row coordinates preserve each dimension separately. All metrics, including automation and CSAT, use FROM tickets; there are no separate message or survey datasets. Pivot tables and heatmaps are not part of this public v1 contract.

Raw ticket fields

Use raw mode when you need one row per ticket instead of aggregated metrics. Replace SHOW and VISUALIZE with an allow-listed SELECT and FORMAT raw:
Raw mode supports FROM tickets, SELECT, a date range, LIMIT, and FORMAT raw. Do not combine it with SHOW, WHERE, GROUP BY, TIMESERIES, COMPARE TO, ORDER BY, SET, or VISUALIZE. DURING and SINCE ... UNTIL ... filter ticket creation time. This differs from aggregate metrics, which use the event named in each metric’s attribution. You can select up to 100 rows per request. Results are ordered by created_at descending and then ticket_id descending. If data.pagination.has_more is true, send data.pagination.next_cursor as the top-level cursor field beside query and timezone in the next request. A cursor is bound to the API agent and exact query. Raw rows exclude deleted, imported, and side tickets. Dashboard permissions and channel, team, and agent restrictions still apply. Raw mode deliberately excludes customer content and complete status-transition history. For custom category fields such as “Contact reason,” use the ticket-field expansion; raw mode does not guess which custom field represents your category.

Grouping and filtering

GROUP BY changes the shape of the result; WHERE removes tickets before aggregation. For example, GROUP BY channel_type returns one row or series per channel type, while WHERE channel_type IN ("email", "whatsapp") includes only email and WhatsApp records. channel_type groups broad transport types, connected_channel groups configured channel connections, and topic_l2 and topic_l3 select different topic hierarchy levels. Use TIMESERIES or a time ID in GROUP BY to choose hour, day, week, or month. LIMIT caps returned category rows; it does not reduce scan cost, change the denominator of percentage metrics, or recompute totals.

Query examples

Send each complete query below as the query string in the HTTP request above, with timezone: "Europe/Vienna". Dates and metric IDs stay in the same format in every example.

Daily ticket volume

Compare tickets created and tickets resolved on each day. These are different event populations, not necessarily the same tickets.

Yesterday’s ticket KPI

Compare new demand with the preceding day.

New tickets by channel

Rank channel types by incoming ticket volume. channel_type returns transport keys such as email and whatsapp; use connected_channel to group by individual connected inboxes.

Tickets by contact reason

Compare new and resolved tickets for selected contact reasons. This example assumes a dropdown ticket field with the stored field_key contact_reason, the display name Contact reason, and the option labels Delivery and Return. Replace these with your workspace’s field key and option labels.
GROUP BY ticket_field("contact_reason") selects a concrete field directly. The alternative SET ticket_field accepts either the stored field key (contact_reason) or the display name (Contact reason). Matching ignores leading/trailing whitespace and letter case. Use the stored field_key returned by the public ticket-field expansion: request include=ticket_fields on GET /public/v1/tickets or GET /public/v1/tickets/:ticketNumber. Do not derive a key from the label by replacing spaces or punctuation; a label can change without changing the stored key. If you explicitly change the field key itself, update your integration queries too. SET ticket_field_values selects option labels within that field, using the same whitespace and case rules. Omit it to include all values for the selected field. The literal "all" is reserved for all fields or all values; selecting specific values requires one field, not "all". If a reference matches more than one entity, the API rejects it with 400 VALIDATION_ERROR. This includes one field’s name matching another field’s key. The canonical v1 language accepts a field key or display name through SET ticket_field. Internal ID-shaped compatibility options are not part of the documented v1 contract. These settings restrict the category breakdown, not the ticket population of a standalone KPI. They do not add support for WHERE filters using custom field keys or option labels.

Contact reason percentages and missing values

Show counts and percentage points together. new_tickets_share divides each category’s distinct new tickets by all eligible new tickets matching the date range, permissions, and WHERE filters. The denominator is calculated before field/option selection, grouping, topN, or LIMIT. For example, 20 Delivery tickets out of 100 matching tickets remain 20%, even if you show only Delivery or limit the result to one row.
SET include_missing is a boolean and is disabled by default. For active, non-archived ticket fields, true adds a Missing bucket for tickets without a usable value, including empty text. Boolean false and numeric 0 are values, not missing. Missing tickets remain eligible even when you select specific option labels; tickets with a different populated option are not relabeled as missing. With all fields selected, missing is calculated separately per field. Limits can still hide that row. Percentages across multiple fields or overlapping values need not sum to 100%; limiting rows never renormalizes them.

Average handle time (AHT)

average_ticket_handle_time estimates how much time agents actively spend handling tickets. For example, while handling a delayed ecommerce order, an agent might update the ticket or assignment, add an internal note, and send the customer a delivery update. Those meaningful recorded actions qualify the work episode. Editing, deleting, or hiding a message also qualifies. A reply or email qualifies only when its linked message has a status of sent, delivered, or read; queued or failed messages do not. Opening or reading the ticket, typing without sending, or clicking alone does not qualify the time. Only one ticket timer runs per person and workspace, across all tabs. Switching tickets moves the timer to the new ticket. Switching to an external tab does not pause it. After 30 minutes without activity, the timer stops and the next activity starts a new episode. An action in an earlier episode does not qualify a later one. Returning to a ticket within the same episode adds to its existing time: 10 minutes on A, 20 on B, and 30 more on A gives A 40 minutes and B 20 minutes, provided each ticket has a qualifying action. A later action on A can qualify all 40 minutes. Reports include running time that has not yet been saved, up to now or the 30-minute inactivity cutoff, whichever comes first. If a qualifying action arrives late, it appears on the next report read. Deleted, imported, and side tickets are excluded. The full session total belongs to the period and time bucket of its effective last observation or end (ended_at, extended by any running clock). Divide those totals by the distinct agent-ticket pairs in that bucket. Sessions spanning midnight or a report boundary are not split minute by minute: a session ending on Tuesday belongs entirely to Tuesday, even if it started on Monday. A later observation can move its entire total into a later bucket. The report end is exclusive. Missing historical tracking is not replaced with resolution time or online time. Query AHT by human agent to compare measured ticket work. Values are returned in seconds. human_agent is the agent attributed to the measured work, not necessarily the ticket’s current assignee or recorded resolver.

First response durations

Compare median waits for a qualifying human or AI reply with waits for a human reply. Both series return seconds; they are not averages.
This example omits responseTimeAttribution and preserves the existing FRT date and value behavior. The legacy date is ticket creation plus the selected stored FRT seconds, not a recorded reply timestamp. Without this option, human_first_response_time_mean also uses the same legacy population as human_first_response_time, even when recorded timestamps are available. Human FRT measures business-hours wait time, so adding it to creation does not establish the actual reply time.

Mean and median human first response

Use the mean to see the effect of unusually long waits, alongside the median for a typical wait. Both use the same eligible human-response observations and return numeric seconds, not formatted time strings. For waits of 60, 120, and 900 seconds, the mean is 360 seconds and the median is 120 seconds. This example explicitly opts into recorded reply dates instead of the legacy date calculation:
SET response_time_attribution = recorded_reply switches all selected FRT metrics (customer_first_response_time, human_first_response_time, human_first_response_time_mean, and ai_first_response_time) to their persisted actual response timestamps. For customer FRT, it selects the earliest qualifying recorded human or AI reply and that reply’s stored duration, requiring both its timestamp and duration rather than comparing human working seconds with AI elapsed seconds. Other metrics in a mixed table keep their own attribution. The option requires at least one FRT metric and is not supported by agent_table. Any other value, including "legacy", or use without an FRT metric returns 400 VALIDATION_ERROR; omit the option for legacy behavior.
Recorded-reply FRT coverage is incomplete. Only opted-in queries exclude observations whose required recorded timestamp is null; there is no per-row fallback to legacy dates. They may return zero when no eligible observations remain, which does not mean replies took zero seconds. The nullable timestamp migration and updated writers collect timestamps but do not backfill history. No historical backfill has been completed as part of this change. Existing reports without the option retain legacy behavior; there is no automatic cutover, even after a future full recomputation or backfill.

CSAT score and sample size

Keep the score next to the number of responses so you can judge small samples. A table allows the decimal score and count in one result.

CSAT sent-cohort response rate by agent

Track surveys sent, how many of those surveys received both a submission and a score, and their response rate. csat_response_rate = 100 × csat_answered_from_sent / csat_sent for the same scope. Later submissions update the original sent-date cohort, even after the report period ends. Do not divide csat_responses by csat_sent: the former uses submission dates and may count surveys sent earlier. With no sent surveys, the rate is zero.
For individual low scores, comments, and stored survey credit, use the raw CSAT export. Raw exports read stored survey credit only. Report CSAT agent breakdowns use stored survey credit when present, otherwise the projected ticket resolver. This existing report fallback does not rewrite surveys, but can put a raw unattributed survey under an agent in a report. Report projections can also be incomplete, so the two surfaces need not reconcile automatically.

Hourly replies and online time

Compare human public messages with online seconds per agent and hour. These are different populations: a person can be online without sending a reply. Rows are the union of observed time/agent pairs across the selected metrics; missing metric cells in those rows are zero, not a full grid of every agent and hour.
Online time clips sessions to the requested period, unions overlapping intervals per agent to avoid double counting, and splits the result across local time buckets. A stale open session ends at its last activity for this aggregate; a recent open session is bounded by now. Daylight-saving days may contain 23 or 25 hours. Repeated local hours have distinct timestamp keys: use keys, not display labels, as identifiers. This is not ticket handling time or a break log; the raw session export deliberately retains the original, unclipped records.

Weekly resolved tickets by agent

Review resolution throughput by recorded resolver and week, rather than tickets created that week.

Monthly agent activity

Keep the agent first in the row dimensions when reviewing monthly replies and online time. Reversing the dimension order changes presentation, not the selected populations.

Automation savings and spend

Compare estimated savings with estimated spend in EUR. This is a same-format daily chart, not a billing reconciliation. These specialized automation metrics support KPIs, daily charts or tables, and action breakdowns except for AI spend. Do not mix them with unrelated ticket metrics or add category splits.

Filtered team workload

Review resolved German-language tickets by team, including resolution duration. Repeated filters are combined with AND. status is the current ticket status, not its status at the time of creation. For filters such as team, channel_type, connected_channel, or human_agent, use your workspace’s entity IDs rather than display names.

Current customer-waiting snapshot

Count open tickets waiting for information from the customer, not customers waiting for support. The date clause is required by the language but does not turn a snapshot into historical data. Do not add TIMESERIES or COMPARE TO to snapshots.

Read results

These are illustrative response shapes with invented values, not guaranteed results for the example periods. Rows and points are shortened. Use returned labels and keys rather than assuming translated labels or fixed channel IDs. A KPI returns a number and its format. previousValue can be null; historical KPIs may already include a prior-period value without an explicit comparison. An explicit COMPARE TO selects the comparison period. Snapshot KPIs have no historical comparison.
Time-series point values use keys from series. Explicit comparisons add series keys ending in __compare.
A category result contains labeled values.
A table describes each column’s metric and format. Do not add or average daily percentages, medians, or averages to reconstruct a period KPI; query the KPI for that period instead.
For time-by-agent tables, rowDimension remains the first requested dimension. The additive rowDimensions array lists both dimensions in request order, and each row adds a dimensions map with separate keys and labels. The combined row key is opaque. Single-dimension tables keep the previous shape; clients must tolerate the new optional fields. The optional average row is a display summary, not a recomputed period metric; use SET show_average_row = false when you do not want it. coordinates is the canonical location for table coordinates; dimensions remains a compatibility field for existing clients.

Metric reference

All 58 registered metric IDs are listed below, grouped as in the discovery catalog. Aggregation, format, and attribution are the exact catalog values. You select the metric, not a custom aggregation function.
  • count counts the defined tickets, messages, surveys, actions, or events; sum adds values; average is an arithmetic mean; median is the middle observation; ratio divides the eligible numerator by its denominator.
  • duration-seconds returns seconds, currency-eur returns EUR, decimal returns a decimal value, and percent returns percentage points: 75 means 75%, not 0.75. CSAT scores use a 1-5 scale.
  • Attribution chooses the date population: ticket_created is a creation cohort; ticket_resolved, reply_event, reopen_event, reassignment_event, survey_submitted, action_executed, and order_paid refer to their respective events. agent_presence measures online-session time; mixed_events combines creation and resolution; current_snapshot means now.
  • Creation-cohort facts can change as tickets develop. In particular, messages and messages_per_ticket include recorded messages on tickets created in the period, not only messages sent during it. Use agent_messages for human public messages sent in the period.
  • Current snapshots cannot use time dimensions or historical comparisons. Backlog age is measured from creation, not from the last message. Even with an older date clause, a snapshot is not an end-of-period backlog reconstruction.
  • Rates use the denominators in their definitions below, within the applicable scope. A zero or empty result alone does not establish that no activity occurred: reporting data can be incomplete or awaiting updates. Validate important totals before using them for commitments or financial reporting.
  • csat_sent and csat_answered_from_sent use survey_sent attribution. csat_response_rate also uses the sent-survey cohort, despite retaining its existing catalog attribution survey_submitted. Score and submitted-response metrics use submission dates.
  • time_saved and money_saved are estimates based on your workspace’s action-time and hourly-rate assumptions. ai_spend converts recorded AI credits using a fallback EUR-per-credit rate; it is not your invoice or a guarantee of your contracted price. Although its catalog attribution is action_executed, spend is dated by AI run creation, including spend on days without tracked actions.

Volume and workload

Response and resolution

Quality and satisfaction

AI and automation

Troubleshooting

Date-range validation, an estimated query-cost check, and returned row/point limits do not guarantee a universal execution deadline or a cap on every result cell or underlying database operation. LIMIT trims returned rows or points; it is not pagination or a guarantee of less database work. Split large reporting jobs into smaller date windows. Historical completeness and freshness depend on the available reporting data; a successful response does not certify either. If a valid small query fails or totals look incomplete, contact support with the response request_id, timezone, and a redacted query. Never share your API key.