> Source: https://builder-docs.ema.ai/agent-qa/api-csat-ingestion
> Title: CSAT Ingestion API

# CSAT Ingestion API

The CSAT Ingestion API uploads a customer satisfaction score or free-text feedback for a call that has already been submitted to Agent QA via the [Audio Ingestion API](/legacy-docs/agent-qa/api-audio-ingestion) or [Transcript Ingestion API](/legacy-docs/agent-qa/api-transcript-ingestion). Ema uses the CSAT signal to enrich the call's QA evaluation.

The `resource_id` and `agent_id` in the CSAT payload must exactly match the values used during the original upload — they are how Ema joins the CSAT to the right call.

## 15.1 Authentication & Headers

Header

Type

Required

Description

`Authorization`

string

Yes

Bearer token (e.g., `Bearer <token>`)

`Content-Type`

string

Yes

`application/json`

> [INFO]
> Note — unlike the Audio and Transcript Ingestion APIs, CSAT Ingestion does **not** use the `x-persona-id` header. The Agent QA persona is passed as `persona_id` in the JSON body.

## 15.2 Upload CSAT

Property

Value

URL

`{base_url}/api/agent-qa/upload/csat`

HTTP Method

POST

Content-Type

`application/json`

### Body Fields

Field

Type

Required

Description

`resource_id`

string

Yes

External identifier for the call. **Must match** the `resource_id` used when uploading the voice file or transcript.

`agent_id`

string

Yes

Identifier of the human agent associated with the call. **Must match** the `agent_id` used when uploading the voice file or transcript.

`csat_input`

string

Yes

A numeric score (`"1"` through `"5"`), free-text feedback, or a single emoji. If parseable as an integer 1–5, it is treated as a numeric score. An emoji is interpreted using your tenant's emoji-to-score mapping. Anything else is treated as free-text feedback. The value must be sent as a string.

`persona_id`

string

Yes

The unique identifier of the Agent QA AI Employee (persona).

### Example Request

```bash
curl -X POST \
  "{base_url}/api/agent-qa/upload/csat" \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "resource_id": "resource_id_123",
    "agent_id": "human_agent_001",
    "persona_id": "<persona_id>",
    "csat_input": "4"
  }'
```

### Success Response (`200 OK`)

The response indicates whether CSAT evaluation was triggered immediately or deferred. If the Agent QA workflow for the call has already finished, evaluation is triggered right away:

```json
{
  "status": "ok",
  "message": "CSAT uploaded and evaluation triggered",
  "csat_triggered": true
}
```

If the Agent QA workflow has not yet completed, the CSAT is stored and evaluation is automatically triggered once the workflow finishes. You do not need to re-upload the CSAT in this case:

```json
{
  "status": "ok",
  "message": "CSAT uploaded, evaluation will be triggered after call analysis completes",
  "csat_triggered": false
}
```

## 15.3 Error Codes

HTTP Code

Reason

`400 Bad Request`

Missing required fields, invalid UUID format, or no matching call found for the provided `resource_id` and `agent_id`.

`401 Unauthorized`

Missing or invalid `Authorization` header.

`403 Forbidden`

Credentials do not have permission to access the specified persona.

`429 Too Many Requests`

Rate limit exceeded. Retry with exponential backoff.

`500 Internal Server Error`

Unexpected server-side error.

> [WARNING]
> **CSAT requires a CSAT persona to be configured for your tenant.** If none is configured, this endpoint does **not** return an error status code — it returns `200 OK` with an error in the response body instead:

```json
{
  "status": "error",
  "message": "No CSAT persona configured for this tenant"
}
```

Always check the `status` field in the response body, not just the HTTP status code — a client that only checks for a non-200 status will silently miss this failure. Ask your Ema representative to configure a CSAT persona for your tenant if you see this message.

## 15.4 Sample Python

```python
import requests

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}",
    "Content-Type": "application/json",
}

def upload_csat(resource_id, agent_id, persona_id, csat_input):
    """Uploads a CSAT score or free-text feedback for a previously ingested call.

    Args:
        resource_id: Must match the resource_id used when uploading the voice file.
        agent_id:    Must match the agent_id used when uploading the voice file.
        persona_id:  The Agent QA AI Employee (persona) ID.
        csat_input:  A string — either a numeric score ("1"–"5") or free-text feedback.
                     Values outside 1–5 are treated as free-text.
    """
    url = f"{BASE_URL}/api/agent-qa/upload/csat"
    payload = {
        "resource_id": resource_id,
        "agent_id": agent_id,
        "persona_id": persona_id,
        "csat_input": csat_input,
    }
    response = requests.post(url, headers=headers, json=payload)
    if response.status_code == 200:
        result = response.json()
        print(f"Success! csat_triggered: {result.get('csat_triggered')}")
        return result
    print(f"Failed! Status: {response.status_code}")
    print(f"Error: {response.text}")
    return None

if __name__ == "__main__":
    # Example 1 — CSAT as a numeric score ("1"–"5" as a string).
    upload_csat(
        resource_id="resource_id_123",
        agent_id="human_agent_001",
        persona_id=AI_EMPLOYEE_ID,
        csat_input="4",
    )

    # Example 2 — CSAT as free-text feedback.
    upload_csat(
        resource_id="resource_id_456",
        agent_id="human_agent_002",
        persona_id=AI_EMPLOYEE_ID,
        csat_input="The agent was very helpful and resolved my issue quickly.",
    )
```
