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

HeaderTypeRequiredDescription
AuthorizationstringYesBearer token (e.g., Bearer <token>)

14.2 Get Daily Export Data

PropertyValue
URL{base_url}/api/agent-qa/get_daily_data
HTTP MethodGET

Query Parameters

FieldTypeRequiredDescription
persona_idUUIDYesThe unique identifier of the AI Employee (persona). Found in the URL of the Ema web app.
datestringYesUTC 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:

FieldTypeDescription
signed_urlstringPresigned download URL for the daily data export file (JSONL format).
manifest_signed_urlstringPresigned download URL for the manifest JSON file.
expires_in_secondsintegerNumber 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.

FieldTypeRequiredDescription
resource_idstringYesUnique external call identifier sent by the client (e.g., a Call ID).
agent_idstringYesIdentifier of the human agent involved in the call.
agent_emailstringNoEmail of the agent. Used for permissioning.
call_timetimestampYesISO 8601 UTC timestamp of when the call occurred.
persona_idstringYesIdentifier of the Agent QA AI Employee that processed this call.
agent_mapping_idstring (UUID)NoThe 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_scorefloatNoCalculated QA score on a 0–100 scale. Null if evaluation did not complete.
is_score_provisionalboolNoTrue 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_countintegerNoNumber of rule results still waiting for a reviewer decision on this call.
flagged_review_countintegerNoTotal number of rule results that were flagged for review on this call, resolved or not.
late_review_countintegerNoNumber 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.
sentimentenumNoOverall sentiment. One of: POSITIVE, NEGATIVE, NEUTRAL.
resolution_statusenumNoWhether the issue was resolved. One of: RESOLVED, NOT_RESOLVED.
contact_reason_categorylist[string]NoOrdered list of categories explaining why the customer contacted support.
call_driver_reasonstringNoDetailed free-text reason for the call.
non_resolution_reason_categorylist[string]NoReasons the call was not resolved. Only populated when resolution_status is NOT_RESOLVED.
agent_talk_timefloatNoDuration (seconds) the agent was speaking.
user_talk_timefloatNoDuration (seconds) the user was speaking.
dead_airfloatNoDuration (seconds) of silence/dead air.
total_audio_timefloatNoTotal audio duration (seconds).
case_idstringNoTicket/case ID associated with the call.
agent_tenureintegerNoAgent tenure in days.
rule_resultslist[object]YesList of QA rule evaluation results. See below.
insightslist[object]YesList of behavioural insights. See below.
transcriptlist[object]YesThe 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

FieldTypeDescription
insight_typeenumOne of: STRENGTH, WEAKNESS, BUG, FEEDBACK, COMPETITOR.
driverstringHigh-level behavioural driver (e.g., "Information verification").
categorystringSub-category within the driver (e.g., "Active listening").
justificationstringExplanation for why this insight was surfaced.
verbatimstringTranscript snippet supporting this insight.

Rule Result Attributes

FieldTypeDescription
rule_namestringName of the QA rule being evaluated.
rule_categorystringGrouping of rules (e.g., "Customer experience", "Call management").
rule_weightfloatWeighting of this rule when calculating the final score.
is_criticalboolWhether this is a critical rule. If any critical rule fails, the recording receives a final score of 0.
rule_mechanismenumInformation source used. One of: TRANSCRIPT, TRANSCRIPT_WITH_KNOWLEDGE_BASE.
resultstringFor 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.
rationalestringExplanation of why this rule passed, failed, or was not applicable.
verbatimslist[string]Transcript snippets supporting the evaluation result.
rule_typestringOne of: BOOLEAN, or the typed rule type. BOOLEAN for legacy rows predating typed rules.
answerstringFor typed rules: the answer value, identical to result. Null for boolean rules.
raw_outputstringRaw model output for the rule, before parsing. Null for boolean rules.
review_statestringManual review state of this rule result. One of: UNFLAGGED, FLAGGED, RESOLVED.
confidencestringThe 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_sufficiencyboolWhether the model had enough data to evaluate this rule. Null when the model did not report a sufficiency value.
feedbackslist[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.

FieldTypeDescription
feedback_typestringOne 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_contentstringThe reviewer's comment or correction, if provided.
is_positiveboolFor USER_FEEDBACK rows: true for thumbs-up, false for thumbs-down. False on a review decision that changed the answer.
decisionobjectFor 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_emailstringEmail address of the user or reviewer who submitted this feedback.
created_attimestampISO 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.

FieldTypeDescription
tenant_idstringThe Ema tenant that owns this export.
persona_idstringThe AI Employee persona for this export partition.
datestringCalendar date of the export partition in YYYY-MM-DD format.
window_startstringISO 8601 UTC start of the export window (inclusive).
window_endstringISO 8601 UTC end of the export window (exclusive).
row_countintegerNumber of call records written to the data file.

14.4 Error Codes

HTTP CodeReason
400 Bad RequestInvalid persona_id format or other malformed request parameters.
401 UnauthorizedMissing or invalid Authorization header.
403 ForbiddenCredentials do not have edit-level permission to access the specified persona.
404 Not FoundPersona does not exist, or no export is available for the requested date.
422 Unprocessable Entitydate is not in YYYY-MM-DD format, or is today/future (must be a past UTC date).
400 Bad RequestThe 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 ErrorUnexpected 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:

  1. Requesting the export starts a background build and returns a request ID immediately.
  2. The UI polls status until the build completes.
  3. 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.

Last updated: Nov 5, 2026