Daily Data Export API
The Daily Data Export API returns presigned download URLs for an AI Employee's Agent QA daily export and its manifest file. The export is generated nightly by Ema's internal workflow and covers all calls processed on the specified UTC date.
Use this API to pull evaluation results, scorecards, and insights into your own data warehouse, BI tool, or downstream analytics system.
14.1 Authentication & Headers
| Header | Type | Required | Description |
|---|---|---|---|
Authorization | string | Yes | Bearer token (e.g., Bearer <token>) |
14.2 Get Daily Export Data
| Property | Value |
|---|---|
| URL | {base_url}/api/agent-qa/get_daily_data |
| HTTP Method | GET |
Query Parameters
| Field | Type | Required | Description |
|---|---|---|---|
persona_id | UUID | Yes | The unique identifier of the AI Employee (persona). Found in the URL of the Ema web app. |
date | string | Yes | UTC date to retrieve, in YYYY-MM-DD format. Must be strictly in the past (UTC). Today or future dates are rejected. |
Example Request
curl -X GET \
"{base_url}/api/agent-qa/get_daily_data?persona_id=<persona_id>&date=2025-06-01" \
-H "Authorization: Bearer <token>"
Response
A successful response (200 OK) returns a JSON object with the following fields:
| Field | Type | Description |
|---|---|---|
signed_url | string | Presigned download URL for the daily data export file (JSONL format). |
manifest_signed_url | string | Presigned download URL for the manifest JSON file. |
expires_in_seconds | integer | Number of seconds until the presigned URLs expire. Currently 3600 (1 hour). |
14.3 File Structure
The two presigned URLs point to files stored in Ema's internal cloud storage. One is a data file containing the actual records and results. The other is a manifest file describing the export partition. URLs expire within an hour; re-request if needed.
Data File (signed_url → agent_qa_export.jsonl)
The data file is JSONL (newline-delimited JSON). Each line is a self-contained JSON object representing one processed call record, corresponding to a single resource_id. The schema is defined by AgentQACallRecord.
| Field | Type | Required | Description |
|---|---|---|---|
resource_id | string | Yes | Unique external call identifier sent by the client (e.g., a Call ID). |
agent_id | string | Yes | Identifier of the human agent involved in the call. |
agent_email | string | No | Email of the agent. Used for permissioning. |
call_time | timestamp | Yes | ISO 8601 UTC timestamp of when the call occurred. |
persona_id | string | Yes | Identifier of the Agent QA AI Employee that processed this call. |
agent_mapping_id | string (UUID) | No | The agent_qa_mappings row resolved at ingestion time for this call. Written once on insert and never rewritten. Null for records created before this field was introduced. |
final_score | float | No | Calculated QA score on a 0–100 scale. Null if evaluation did not complete. |
is_score_provisional | bool | No | True when the score counts a flagged pass/fail rule result that has not yet been reviewed. A reviewer's decision can change the score. False once all flagged pass/fail results are resolved. |
unresolved_review_count | integer | No | Number of rule results still waiting for a reviewer decision on this call. |
flagged_review_count | integer | No | Total number of rule results that were flagged for review on this call, resolved or not. |
late_review_count | integer | No | Number of reviewer decisions on this call that were made after the call's original export date. When this is greater than zero, the call re-appears in the next day's nightly export so that the decisions reach your data warehouse. |
sentiment | enum | No | Overall sentiment. One of: POSITIVE, NEGATIVE, NEUTRAL. |
resolution_status | enum | No | Whether the issue was resolved. One of: RESOLVED, NOT_RESOLVED. |
contact_reason_category | list[string] | No | Ordered list of categories explaining why the customer contacted support. |
call_driver_reason | string | No | Detailed free-text reason for the call. |
non_resolution_reason_category | list[string] | No | Reasons the call was not resolved. Only populated when resolution_status is NOT_RESOLVED. |
agent_talk_time | float | No | Duration (seconds) the agent was speaking. |
user_talk_time | float | No | Duration (seconds) the user was speaking. |
dead_air | float | No | Duration (seconds) of silence/dead air. |
total_audio_time | float | No | Total audio duration (seconds). |
case_id | string | No | Ticket/case ID associated with the call. |
agent_tenure | integer | No | Agent tenure in days. |
rule_results | list[object] | Yes | List of QA rule evaluation results. See below. |
insights | list[object] | Yes | List of behavioural insights. See below. |
transcript | list[object] | Yes | The diarized call transcript as an ordered list of utterances, each {role, message, start, end} (start/end in seconds from call start). A successfully transcribed call with no utterances exports as an empty list, not an error. |
Insight Attributes
| Field | Type | Description |
|---|---|---|
insight_type | enum | One of: STRENGTH, WEAKNESS, BUG, FEEDBACK, COMPETITOR. |
driver | string | High-level behavioural driver (e.g., "Information verification"). |
category | string | Sub-category within the driver (e.g., "Active listening"). |
justification | string | Explanation for why this insight was surfaced. |
verbatim | string | Transcript snippet supporting this insight. |
Rule Result Attributes
| Field | Type | Description |
|---|---|---|
rule_name | string | Name of the QA rule being evaluated. |
rule_category | string | Grouping of rules (e.g., "Customer experience", "Call management"). |
rule_weight | float | Weighting of this rule when calculating the final score. |
is_critical | bool | Whether this is a critical rule. If any critical rule fails, the recording receives a final score of 0. |
rule_mechanism | enum | Information source used. One of: TRANSCRIPT, TRANSCRIPT_WITH_KNOWLEDGE_BASE. |
result | string | For boolean rules: one of PASS, FAIL, NOT_APPLICABLE. For typed (categorical/descriptive) rules: the actual answer value (e.g., the selected category or descriptive text). Falls back to OUTCOME_IN_DETAILS when no answer was recorded. |
rationale | string | Explanation of why this rule passed, failed, or was not applicable. |
verbatims | list[string] | Transcript snippets supporting the evaluation result. |
rule_type | string | One of: BOOLEAN, or the typed rule type. BOOLEAN for legacy rows predating typed rules. |
answer | string | For typed rules: the answer value, identical to result. Null for boolean rules. |
raw_output | string | Raw model output for the rule, before parsing. Null for boolean rules. |
review_state | string | Manual review state of this rule result. One of: UNFLAGGED, FLAGGED, RESOLVED. |
confidence | string | The model's confidence in this result, expressed as a bucket: HIGH (0.85 or above), MEDIUM (0.70 to under 0.85), or LOW (under 0.70). Absent when the model did not report a confidence score. |
data_sufficiency | bool | Whether the model had enough data to evaluate this rule. Null when the model did not report a sufficiency value. |
feedbacks | list[object] | Feedback and reviewer decisions on this rule result. See below. |
Feedback Attributes
Each object in a rule result's feedbacks list represents either a user thumbs-up/down rating or a reviewer decision. The feedback_type field distinguishes between the two kinds.
| Field | Type | Description |
|---|---|---|
feedback_type | string | One of: USER_FEEDBACK (a thumbs-up or thumbs-down rating left by a user) or LOW_CONFIDENCE_FEEDBACK (a reviewer decision on a flagged result). |
feedback_content | string | The reviewer's comment or correction, if provided. |
is_positive | bool | For USER_FEEDBACK rows: true for thumbs-up, false for thumbs-down. False on a review decision that changed the answer. |
decision | object | For LOW_CONFIDENCE_FEEDBACK rows only: the reviewer's decision as JSON. For pass/fail rules, includes whether the reviewer confirmed or overrode the result and the chosen verdict. For category rules, includes whether the reviewer confirmed or picked/proposed a category. For written-answer rules, includes whether the reviewer accepted or rewrote the answer. Null on USER_FEEDBACK rows. |
feedback_user_email | string | Email address of the user or reviewer who submitted this feedback. |
created_at | timestamp | ISO 8601 UTC timestamp of when this feedback was submitted. |
Reviewer decisions (LOW_CONFIDENCE_FEEDBACK) are exported in the feedbacks list alongside user ratings, oldest first. When a reviewer changes a decision, every decision row is exported so the full history is available. Use feedback_type to separate user ratings from reviewer decisions in your downstream processing. Feedback counts, the feedback funnel, and Auto-Learning reads in Agent QA use USER_FEEDBACK rows only; reviewer decisions are stored separately for future learning integration.
For typed (categorical/descriptive) rules, result now contains the answer value directly, the same value as answer. In earlier exports, result carried the placeholder OUTCOME_IN_DETAILS for these rows. Any unparsed payload fields from the rule's named output that are not part of the standard set are kept under the rule's display name in the export record.
Late Reviews
A reviewer decision made after a call's original export date is called a late review. When one or more late reviews exist on a call, the call re-appears in the next nightly export with updated scores and decision data. The late_review_count field on the call record indicates how many late reviewer decisions it carries. This is separate from late_feedback_count, which covers late user ratings.
Manifest File (manifest_signed_url → manifest.json)
The manifest file is a single JSON object describing the export partition. It is written alongside the data file after the nightly export completes successfully. The schema is defined by AgentQAExportManifest.
| Field | Type | Description |
|---|---|---|
tenant_id | string | The Ema tenant that owns this export. |
persona_id | string | The AI Employee persona for this export partition. |
date | string | Calendar date of the export partition in YYYY-MM-DD format. |
window_start | string | ISO 8601 UTC start of the export window (inclusive). |
window_end | string | ISO 8601 UTC end of the export window (exclusive). |
row_count | integer | Number of call records written to the data file. |
14.4 Error Codes
| HTTP Code | Reason |
|---|---|
400 Bad Request | Invalid persona_id format or other malformed request parameters. |
401 Unauthorized | Missing or invalid Authorization header. |
403 Forbidden | Credentials do not have edit-level permission to access the specified persona. |
404 Not Found | Persona does not exist, or no export is available for the requested date. |
422 Unprocessable Entity | date is not in YYYY-MM-DD format, or is today/future (must be a past UTC date). |
400 Bad Request | The requested date is older than your tenant's data retention window; see 2.8 Additional Configuration Tab Settings for retention periods. This applies regardless of whether a partition for that date still physically exists. |
500 Internal Server Error | Unexpected server-side error. |
14.5 Rate Limits
A rate limit of 1 query per second is enforced on this API.
14.6 Sample Python
The snippet below downloads and parses agent_qa_export.jsonl from the presigned URL returned by the API.
import json
import urllib.request
# Step 1 — call the API to get the presigned URLs
req = urllib.request.Request(
"https://api.<region>.ema.co/api/agent-qa/get_daily_data"
"?persona_id=<persona_id>&date=2025-06-01",
headers={
"Authorization": "Bearer <token>",
"x-persona-id": "<persona_id>",
},
)
with urllib.request.urlopen(req) as resp:
urls = json.load(resp)
signed_url = urls["signed_url"]
manifest_signed_url = urls["manifest_signed_url"]
# Step 2 — download and parse the JSONL data file
records = []
with urllib.request.urlopen(signed_url) as resp:
for line in resp:
line = line.strip()
if line:
records.append(json.loads(line))
# Step 3 — work with the records
for record in records:
resource_id = record["resource_id"]
agent_id = record["agent_id"]
final_score = record.get("final_score")
sentiment = record.get("sentiment")
resolution = record.get("resolution_status")
agent_mapping_id = record.get("agent_mapping_id")
rule_results = record["rule_results"]
insights = record["insights"]
print(f"{resource_id} | agent={agent_id} | score={final_score} | {sentiment} | {resolution}")
for rule in rule_results:
print(f" [{rule['result']}] {rule['rule_name']} (weight={rule['rule_weight']})")
for feedback in rule.get("feedbacks", []):
fb_type = feedback.get("feedback_type")
if fb_type == "LOW_CONFIDENCE_FEEDBACK":
print(f" [REVIEW] {feedback.get('feedback_user_email')} — {feedback.get('decision')}")
else:
print(f" [FEEDBACK] {'👍' if feedback.get('is_positive') else '👎'} {feedback.get('feedback_user_email')}")
for insight in insights:
print(f" [{insight['insight_type']}] {insight['driver']} — {insight['category']}")
# Step 4 — download and parse the manifest
with urllib.request.urlopen(manifest_signed_url) as resp:
manifest = json.load(resp)
print(f"Export date : {manifest['date']}")
print(f"Window : {manifest['window_start']} → {manifest['window_end']}")
print(f"Row count : {manifest['row_count']}")
14.7 On-Demand Current-Day Export
The nightly export (above) always covers a past, completed UTC day. To pull data from the current calendar day before the next nightly run, request the live export from the Audit tab's export action. It is delivered asynchronously rather than as a direct download, since a busy day's export can take up to a minute to build:
- Requesting the export starts a background build and returns a request ID immediately.
- The UI polls status until the build completes.
- Once complete, a signed download URL is provided, valid for 1 hour.
Today's data is a partial snapshot. Calls still being processed won't appear until a later export. This is expected: the current-day export is for freshness, not completeness. Use the nightly export for a fully settled day.