Transcript Ingestion API
The Transcript Ingestion API uploads a pre-existing text transcript for processing by the Agent QA AI Employee. Use this endpoint when audio is not available and you already have the conversation transcript in JSON, XML, or HTML format.
If you have an audio file instead, use the Audio Ingestion API.
13.1 Authentication & Headers
All requests require the same headers as the Audio Ingestion API:
| Header | Type | Required | Description |
|---|---|---|---|
Authorization | string | Yes | Bearer token (e.g., Bearer <token>) |
x-persona-id | UUID | Yes | The unique identifier of the Agent QA AI Employee (persona). |
13.2 Upload Transcript
| Property | Value |
|---|---|
| URL | {base_url}/api/v1/external/upload/transcript |
| HTTP Method | POST |
| Content-Type | application/json |
Body Fields
| Field | Type | Required | Description |
|---|---|---|---|
format | string | Yes | Format of raw_transcript. Supported values: json, xml, html. |
raw_transcript | string | Yes | Full transcript serialised as a string in the specified format. See format examples below. |
agent_id | string | Yes | Identifier of the human agent associated with the recording. |
agent_email | string | Yes | The human agent's email. Used for permissioning. |
resource_id | string | Yes | External identifier (e.g., Call ID from your telephony provider). |
language | string | No | Language of the transcript. |
timestamp | string | Yes | Call timestamp. Values without timezone information are treated as UTC. |
case_id | string | No | Ticket/case ID associated with the call. |
customer_survey | JSON | No | Customer satisfaction/dissatisfaction survey data. |
agent_tenure | integer | No | Tenure of the associated agent in days. |
json format
A JSON array where each element is one utterance. The time field is optional and accepts ISO 8601, HH:MM:SS, HH:MM, and common date-time variants. When omitted, all utterances are assigned a timestamp of 0.
[
{
"time": "09:15:00",
"role": "Agent",
"text": "Hello, how can I help you?"
},
{
"time": "09:15:05",
"role": "Customer",
"text": "I have an issue with my account."
}
]
xml or html format
An XML chat document. The time attribute is optional.
<chat>
<message time="09:15:00" role="Agent">Hello, how can I help you?</message>
<message time="09:15:05" role="Customer">I have an issue with my account.</message>
</chat>
Success Response (200 OK)
{
"file_id": "550e8400-e29b-41d4-a716-446655440000",
"file_name": "resource_id_123",
"status": "UPLOAD_COMPLETED"
}
13.3 Error Codes
| HTTP Code | Reason |
|---|---|
400 Bad Request | Missing headers, invalid UUID format, unsupported format, or missing agent_id. |
403 Forbidden | Credentials do not have permission to access the specified AI Employee. |
429 Too Many Requests | Rate limit exceeded. Retry with exponential backoff. |
500 Internal Server Error | Unexpected server-side error. |
13.4 Post-Upload Processing Failures
A 200 OK response confirms the file was accepted for processing, but processing failures can still occur asynchronously. One category of failure is specific to transcript ingestion:
CORRUPT_TRANSCRIPT
If the uploaded file carries content but cannot be decoded into utterances, the interaction is failed with the terminal audit state CORRUPT_TRANSCRIPT. Examples that trigger this state:
- A
jsonformat upload whose body is not a JSON array of{time, role, text}objects. - A file whose
formatvalue does not match the actual contents.
CORRUPT_TRANSCRIPT is permanent. Decoding is deterministic: the same bytes will always fail. The interaction will not be retried automatically. You must correct the source file and resubmit it via a new upload request.
This state is distinct from EMPTY_TRANSCRIPT, which is a well-formed transcript that simply contains no utterances.
13.5 Rate Limits
A rate limit of 10 queries per second is enforced on the data upload API.
13.6 Sample Python
import requests
import time
import json
BASE_URL = "https://api.<region>.ema.co" # replace with your domain
AUTH_TOKEN = "YOUR_BEARER_TOKEN"
AI_EMPLOYEE_ID = "YOUR_AI_EMPLOYEE_UUID"
headers = {
"Authorization": f"Bearer {AUTH_TOKEN}",
"x-persona-id": AI_EMPLOYEE_ID,
}
def upload_transcript(transcript, agent_id, agent_email, resource_id,
timestamp, max_retries=3):
"""Uploads a text transcript for a specific AI Employee.
transcript: list of dicts with keys: time (optional), role, text.
"""
url = f"{BASE_URL}/api/v1/external/upload/transcript"
payload = {
"format": "json",
"raw_transcript": json.dumps(transcript),
"agent_id": agent_id,
"agent_email": agent_email,
"resource_id": resource_id,
"timestamp": timestamp,
}
for attempt in range(max_retries + 1):
response = requests.post(url, headers=headers, json=payload)
if response.status_code == 200:
return response.json()
if response.status_code == 429 and attempt < max_retries:
retry_after = int(response.headers.get("Retry-After", 2 ** attempt))
time.sleep(retry_after)
continue
print(f"Failed! Status: {response.status_code}")
print(f"Error: {response.text}")
return None
if __name__ == "__main__":
result = upload_transcript(
transcript=[
{"time": "09:15:00", "role": "Agent", "text": "Hello, how can I help you?"},
{"time": "09:15:05", "role": "Customer", "text": "I have an issue with my account."},
{"time": "09:15:10", "role": "Agent", "text": "I'd be happy to help with that."},
],
agent_id="human_agent_001",
agent_email="[email protected]",
resource_id="resource_id_456",
timestamp=time.strftime("%Y-%m-%d %H:%M:%S"),
)
if result:
print(f"New File ID: {result.get('file_id')}")