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:
| 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— walknext_cursoruntil 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:
usermessages haverun = nulland nosources;assistantmessages with no citations havesources = [](not null).feedbackandrevieware present only when populated.session.deleted = truemeans the session was soft-deleted in the chat UI; messages are still exported so your warehouse can mirror the deletion.schema_versionis on every row (currently always1). Adding a field is non-breaking; removing or reordering is breaking and is announced via aSunsetHTTP 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.
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 — the chatbot that produces this data.
- Integrations Hub — manage channels and connections.