Chat Metrics
Chat metrics let you pull the raw conversational data behind a chat AI Employee (AIE) into your own data warehouse or BI tool. Where the in-app metrics dashboard gives you charts inside Ema, the chat metrics export gives you the underlying rows — one per message — so you can join, aggregate, and analyze them however you need.
The export is served by the chat service over two endpoints: a modern, streaming messages export (32 columns) and a legacy, wire-compatible conversations export (22 columns). Both support CSV; the messages export also supports NDJSON and paginated JSON.
Admin-only, and enabled per environment. Both export endpoints require the system_admin role, and they are gated behind an environment toggle (EMU_CHAT_METRICS_API_ENABLED, off by default): the routes are not registered until your environment is configured for the export. If the endpoints return a not-found, the export isn't enabled in your environment yet.
Messages export (32 columns)
GET /metrics/messages returns row-level conversational data — one row per chat message — with the message joined to the workflow run that produced it, retrieval sources, user feedback, conversation review state, and non-PII user context. This is the export to use for new BI integrations.
Formats
Choose the format with the format query parameter (CSV is the default; the Accept header is honored as a fallback when format is omitted):
format | Behavior |
|---|---|
csv (default) | RFC 4180 CSV, streamed via chunked transfer with Content-Disposition: attachment. One header row, then one row per message. Streams the full filtered range. |
ndjson | One JSON record per line, no envelope, streamed. Streams the full filtered range. |
json | One paginated page: { items, next_cursor, page_info }. You walk next_cursor until the response no longer carries one (the key is omitted on the last page). |
Repeated or unbounded structures (citations, model breakdown, user attributes) are JSON-encoded into single columns (sources_json, model_breakdown_json, user_attributes_json) so a warehouse PARSE_JSON() works without exploding the row count.
Filters
| Parameter | Meaning |
|---|---|
since | Inclusive lower bound on the message created_at (RFC 3339). |
until | Exclusive upper bound on created_at (RFC 3339). Defaults to now. |
updated_since | Switches the time filter to change tracking (RFC 3339, supersedes since): returns messages created at or after this timestamp, plus messages created within the preceding 30 days whose feedback or review state was updated at or after it. Changes to feedback or review on messages older than that 30-day look-back are not returned. |
workflow_id | Restrict to one AI Employee (UUID). |
cursor | Opaque keyset cursor from the previous page's next_cursor. Only valid with format=json. |
limit | Page size for format=json (1–500, default 200). CSV and NDJSON ignore this and stream the full range. |
Supplying cursor or limit with a streaming format returns a validation error (422) — the streaming formats always walk the entire filtered range.
Columns
The CSV header is fixed and ordered — 32 columns. Each row carries these columns:
| Column | Notes |
|---|---|
message_id | |
role | Message author role. |
content | Message text (PII rendered per tenant policy — see below). |
created_at | |
session_id | |
session_title | |
session_started_at | |
session_deleted | |
session_deleted_at | |
channel_type | The channel the conversation came through. |
channel_display_name | |
workflow_id | The AI Employee. |
run_id | The workflow run that produced the message; empty when there is none. |
run_status | |
run_latency_ms | |
run_total_tokens | |
feedback_sentiment | Empty when no feedback. |
feedback_category | |
feedback_text | |
feedback_submitted_at | |
feedback_updated_at | |
review_resolution_status | Conversation review state; empty when not reviewed. |
review_comment_count | |
review_updated_at | |
user_actor_type | |
user_external_id | |
user_display_name | |
user_department | |
user_attributes_json | JSON object; {} when empty. |
sources_json | JSON array of retrieval sources; [] when empty. |
model_breakdown_json | JSON array of per-model token usage; [] when empty. |
schema_version | The export schema version (currently 1). Adding a column at the end is non-breaking; the contract reserves reordering or removal for an announced deprecation with a schema_version bump. |
curl -G 'https://<your-tenant>/api/v1/chat/metrics/messages' \
-H 'Authorization: Bearer <token>' \
--data-urlencode 'format=csv' \
--data-urlencode 'since=2026-05-01T00:00:00Z' \
--data-urlencode 'until=2026-06-01T00:00:00Z' \
--data-urlencode 'workflow_id=8f3c....'
Streaming, limits, and errors
- One streaming export per tenant. CSV and NDJSON responses from
/metrics/messagesare subject to a per-tenant concurrent-stream cap — only one streaming export runs at a time. A second concurrent request gets429with aRetry-After. Paginated JSON is not subject to the cap, and neither is the legacy/metrics/conversationsendpoint (its load is bounded by the 31-day window instead). - Mid-stream truncation. Because streaming sends
200 OKand the header row before all rows are known, an error after streaming has begun can't change the status code. Detect truncation by row count, not by HTTP status. - Slow queries. A query that exceeds the database statement timeout returns
503with aRetry-After; narrow the time window and retry. - Auditing. Every completed export is recorded as an audit event (with format, row count, and the filters used), because content may be returned in cleartext depending on tenant PII policy. A stream that aborts mid-flight is not audited — another reason to detect truncation by row count.
Conversations export (legacy, 22 columns)
GET /metrics/conversations is a wire-compatible replacement for the legacy chatbot/conversations export. It returns one row per chat message in the window, denormalized with conversation, external-user, feedback, and workflow-run-error context, in a fixed 22-column order that matches the legacy exporter exactly — so existing downstream warehouses keep working without a contract change. Unlike the messages export, the response is assembled in memory before it is written (the ≤ 31-day window keeps it bounded) rather than streamed page by page.
Parameters
| Parameter | Meaning |
|---|---|
persona_id (required) | The AI Employee (UUID) — the chat session's workflow_id. |
start_date (required) | Inclusive lower bound, UTC YYYY-MM-DD. |
end_date (required) | Inclusive upper bound, UTC YYYY-MM-DD. The total range must be ≤ 31 days. |
format | csv (default) or json. json returns an array with the same field names as the CSV header. |
An empty range returns 200 with a header-only CSV. An invalid persona_id, malformed dates, or a range over 31 days returns 422.
Columns
The 22 columns, in order:
conversation_id, conversation_source, conversation_created_at, conversation_updated_at, encrypted_user_email, message_id, is_user_message, message_created_at, message_updated_at, workflow_run_error, external_tool_called, external_tool_call_success, external_tool_name, user_feedback_type, user_feedback_comment, feedback_message_sent, content_payload_json, user_context, unique_source_ids, country, band, events_per_message.
Which export to use. Use the messages export for new integrations — it streams, supports NDJSON and JSON, has richer columns, and carries a schema_version. Use the conversations export only when you must match the legacy 22-column contract a downstream warehouse already depends on.
A note on PII and content
Message content and user context may contain personal data, and the two endpoints treat it differently.
- The messages export renders content per your tenant's PII policy: content is returned in cleartext only for tenants that have opted in, and the export never silently falls back to raw cleartext when the PII service is unavailable — it returns
503instead. - The conversations export applies no PII rendering at all: message content, feedback text, and user context are emitted in cleartext for every tenant, preserving wire compatibility with the legacy exporter it replaces.
This is exactly why both endpoints are system_admin-only and every export is audited. Treat exported files as sensitive data and handle them under your organization's data-governance rules.
What's next
- Launching and monitoring — the in-app dashboard view of the same data.
- Debug logs — drill into the individual run behind a
run_idin the export. - API reference — full request and response schemas.