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:

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:

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.

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:

FormatWhen to use itStreaming
csv (default)One-shot download, opening in Excel, simple COPY into a warehouseYes
ndjsonbq load, COPY INTO ... FILE_FORMAT = (TYPE = JSON), Fivetran custom sourcesYes
json (paginated)Ad-hoc inspection, a script that wants explicit paginationNo

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:

ParameterTypeNotes
formatcsv (default) / json / ndjsonOr use the Accept header.
sinceRFC3339 timestampInclusive lower bound on created_at.
untilRFC3339 timestampExclusive upper bound on created_at. Defaults to "now."
updated_sinceRFC3339 timestampSwitches to the 30-day look-back model — recommended for daily syncs.
workflow_idUUIDRestrict to one chatbot.
cursor / limitopaque cursor / 1–500format=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.

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:

{
  "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

HTTPMeaningAction
200OK. Streams may still abort mid-flight — verify your byte/row count.
400 invalid_cursorCursor tampered or expired.Drop it and re-walk from since.
401Missing or wrong API key.Re-issue from the admin UI.
403Caller is not system_admin, or the feature is disabled.Talk to your admin or account contact.
429Rate limited, or another export is already running for your tenant.Honor Retry-After.
503 statement_timeoutTime 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.

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

Last updated: Jul 3, 2026