> Source: https://builder.ema.ai/v2/integrations-data/data-api
> Title: Data API

# Data API

The conversational Data API lets you pull your chatbot conversation data out of Ema and into your own analytics stack — Snowflake, BigQuery, Looker, Power BI, Excel, or a data-science notebook. It returns **one row per chat message**, with the workflow run, retrieval sources, user feedback, review state, and non-PII user context attached, so you can build adoption dashboards, run sentiment or intent analysis, and compute your own metrics without depending on Ema's in-product views.

The API is **read-only**. It is designed for tenant admins, BI engineers, and analysts:

-   **Analyst** — one-shot CSV download, opened in Excel.
-   **BI engineer** — a scheduled daily incremental load into a warehouse.
-   **Admin** — issue keys and decide what PII the export reveals.

## Endpoint and authentication

A single endpoint serves the export:

```http
GET /metrics/messages
```

Through the platform gateway the full URL is:

```
https://chat.<your-domain>/api/chat/metrics/messages
```

Authenticate with an API key in the `X-API-Key` header:

```bash
curl -OJ \
  -H "X-API-Key: emu_..." \
  "https://chat.<your-domain>/api/chat/metrics/messages?since=2026-04-01T00:00:00Z"
```

Only **`system_admin`** API keys can call the metrics API — any other role gets a `403`. The export is also gated by a per-environment feature flag, so if it is not enabled for your environment the route returns `404`.

> [INFO]
> **Admin setup.** Issue a key from the chat admin UI under **Settings → API keys → New API key** with the `system_admin` role. The key is shown once — copy it straight into your BI tool's secret store. Revoking it stops the export within seconds.

## Output formats

The same row shape is available in three formats; pick one based on how you consume it:

Format

When to use it

Streaming

`csv` (default)

One-shot download, opening in Excel, simple `COPY` into a warehouse

Yes

`ndjson`

`bq load`, `COPY INTO ... FILE_FORMAT = (TYPE = JSON)`, Fivetran custom sources

Yes

`json` (paginated)

Ad-hoc inspection, a script that wants explicit pagination

No

Set the format with `?format=` or with the `Accept` header (`text/csv`, `application/json`, `application/x-ndjson`). CSV flattens nested objects into columns and JSON-encodes repeated structures (citations, model breakdown, user attributes) into single `*_json` columns; expand those with your warehouse's `PARSE_JSON()` / `JSON_EXTRACT_*` functions.

## Filters and pagination

All three formats accept the same filters:

Parameter

Type

Notes

`format`

`csv` (default) / `json` / `ndjson`

Or use the `Accept` header.

`since`

RFC3339 timestamp

Inclusive lower bound on `created_at`.

`until`

RFC3339 timestamp

Exclusive upper bound on `created_at`. Defaults to "now."

`updated_since`

RFC3339 timestamp

Switches to the 30-day look-back model — recommended for daily syncs.

`workflow_id`

UUID

Restrict to one chatbot.

`cursor` / `limit`

opaque cursor / 1–500

`format=json` only. CSV and NDJSON ignore them and stream the full filtered range.

Pagination differs by format:

-   **`format=json`** — walk `next_cursor` until it is null. Default page size 200, max 500.
-   **`format=csv` / `format=ndjson`** — no pagination; the response streams the entire `(since, until)` range in one HTTP call. For very large tenants, slice the time window client-side (a day or a week per call).

### Daily warehouse sync

The recommended pattern: run one backfill in `csv` or `ndjson` from `since=<start>` to seed the table, then a daily incremental with `updated_since=<midnight yesterday>` upserting on `message_id`. `updated_since` returns all messages whose `created_at` is at or after the timestamp, plus messages from the previous **30 days** whose `feedback` or `review` state changed. Updates to rows older than 30 days are not re-emitted — run a wider re-sync occasionally if your reviewers act on older conversations.

```bash
SINCE="$(date -u -v-1d +"%Y-%m-%dT00:00:00Z")"
curl -sS --no-buffer \
  -H "X-API-Key: $EMA_API_KEY" \
  "https://chat.<your-domain>/api/chat/metrics/messages?format=ndjson&updated_since=$SINCE" \
  | aws s3 cp - "s3://your-bucket/chat/$(date -u +%FT%H%M%SZ).ndjson"
```

## Row shape

Every row carries these top-level fields, in all three formats:

```json
{
  "message_id": "uuid",
  "schema_version": 1,
  "session": {
    "id": "uuid",
    "title": "How do I reset my VPN…",
    "started_at": "2026-04-12T09:12:00Z",
    "deleted": false,
    "deleted_at": null,
    "channel_type": "teams",
    "channel_display_name": "IT Support"
  },
  "workflow": { "id": "uuid" },
  "user": {
    "actor_type": "internal",
    "external_id": "29:1abc…",
    "display_name": "Anita Rao",
    "department": "Engineering",
    "attributes": { "office": "BLR" }
  },
  "role": "user",
  "content": "…",
  "created_at": "2026-04-12T09:12:01Z",
  "run": {
    "id": "uuid",
    "status": "completed",
    "latency_ms": 12500,
    "total_tokens": 4831,
    "model_breakdown": [
      { "model_id": "claude-sonnet-4-6", "prompt_tokens": 3000, "completion_tokens": 1831 }
    ]
  },
  "sources": [
    { "step_id": "uuid", "document_id": "uuid", "title": "VPN Setup", "uri": "…", "snippet": "…", "rank": 1 }
  ],
  "feedback": {
    "sentiment": "positive",
    "category": "answer_quality",
    "text": "This worked, thanks",
    "submitted_at": "…",
    "updated_at": "…"
  },
  "review": {
    "resolution_status": "resolved",
    "comment_count": 3,
    "updated_at": "…"
  }
}
```

Notes:

-   `user` messages have `run = null` and no `sources`; `assistant` messages with no citations have `sources = []` (not null).
-   `feedback` and `review` are present only when populated.
-   `session.deleted = true` means the session was soft-deleted in the chat UI; messages are still exported so your warehouse can mirror the deletion.
-   `schema_version` is on every row (currently always `1`). Adding a field is non-breaking; removing or reordering is breaking and is announced via a `Sunset` HTTP header at least six months ahead.

### CSV column order

For `format=csv` the header row is:

```
message_id, role, content, created_at,
session_id, session_title, session_started_at, session_deleted, session_deleted_at,
channel_type, channel_display_name,
workflow_id,
run_id, run_status, run_latency_ms, run_total_tokens,
feedback_sentiment, feedback_category, feedback_text, feedback_submitted_at, feedback_updated_at,
review_resolution_status, review_comment_count, review_updated_at,
user_actor_type, user_external_id, user_display_name, user_department,
user_attributes_json, sources_json, model_breakdown_json,
schema_version
```

## PII and user attributes

The Data API runs against the **PII egress allow-list** — the same control that gates Slack, email, and other outbound integrations. By default everything content-bearing is **tokenized**: message bodies, feedback text, citation snippets, and user display names come back as `EMA_PII_<token>` rather than raw values. The response also carries `X-Pii-Mode: tokenized` for programmatic detection.

To return cleartext content, an admin opts the tenant into the `metrics_export` integration in the PII admin UI (**Settings → PII → Egress allow-list → Add integration → `metrics_export` → Allow**). Many BI use cases never need raw content and can leave this off.

User attributes are opt-in per channel. By default `user_attributes_json` is `{}`. An admin lists the keys to export under **Channels → <your channel> → Metrics export attributes**; only those keys appear, and any PII-bearing values still flow through the egress allow-list above.

## Errors, rate limits, and SLAs

HTTP

Meaning

Action

`200`

OK. Streams may still abort mid-flight — verify your byte/row count.

—

`400 invalid_cursor`

Cursor tampered or expired.

Drop it and re-walk from `since`.

`401`

Missing or wrong API key.

Re-issue from the admin UI.

`403`

Caller is not `system_admin`, or the feature is disabled.

Talk to your admin or account contact.

`429`

Rate limited, or another export is already running for your tenant.

Honor `Retry-After`.

`503 statement_timeout`

Time window too large for one query.

Chunk by a smaller window.

`503` (other)

The PII service is unreachable; Ema never falls back to raw content.

Retry; honor `Retry-After`.

Rate limits: paginated JSON is 60 requests/minute per API key; streaming CSV/NDJSON is **one in-flight stream per tenant** (a second concurrent stream returns `429`), and a stream runs for up to one hour.

> [WARNING]
> Streaming responses return HTTP `200` before all rows are written. If a stream fails mid-flight the connection closes without a trailing record — Ema deliberately does not append an in-band error sentinel, because that would corrupt warehouse loads. Defensive loaders should verify the last byte is a newline, compare row count against an expected lower bound, and retry on a fresh window if either check fails.

Known SLAs and omissions: feedback/review updates older than 30 days are not returned by `updated_since`; rows that disappear between syncs reflect your tenant's retention horizon; DBA-driven row deletes are not tracked (only soft-deleted sessions are); and exports run on a read replica so they never slow down live conversations.

## What's next

-   [Embeddable Chat SDK](/builder/v2/integrations-data/embeddable-chat-sdk) — the chatbot that produces this data.
-   [Integrations Hub](/builder/v2/integrations-data/integrations-hub) — manage channels and connections.
