> ## Documentation Index
> Fetch the complete documentation index at: https://docs.cekura.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# LiveKit (Manual)

> Run evaluator scenarios by manually providing LiveKit room URL and access token (voice and chat modes)

export const CopyPageButton = () => {
  if (typeof window !== 'undefined') {
    setTimeout(function () {
      if (document.getElementById('ck-tools')) return;
      var anchor = document.getElementById('content-area') || document.querySelector('.mdx-content');
      if (!anchor) return;
      if (!document.getElementById('ck-style')) {
        var s = document.createElement('style');
        s.id = 'ck-style';
        s.textContent = '#ck-tools{position:absolute;top:6px;right:0;z-index:100;font-family:inherit;}' + '.ck-row{display:inline-flex;align-items:stretch;border:1px solid rgba(0,0,0,0.15);border-radius:8px;overflow:hidden;background:#fff;}' + ':root.dark .ck-row{background:rgba(255,255,255,0.06);border-color:rgba(255,255,255,0.12);}' + '.ck-btn{padding:5px 12px;border:none;background:none;cursor:pointer;font-size:13px;font-weight:500;font-family:inherit;color:#374151;}' + ':root.dark .ck-btn{color:#d1d5db;}' + '.ck-btn:hover{background:rgba(0,0,0,0.04);}' + ':root.dark .ck-btn:hover{background:rgba(255,255,255,0.06);}' + '.ck-chevron{padding:5px 8px;border:none;background:none;cursor:pointer;font-size:14px;font-family:inherit;color:#374151;}' + ':root.dark .ck-chevron{color:#d1d5db;}' + '.ck-chevron:hover{background:rgba(0,0,0,0.04);}' + ':root.dark .ck-chevron:hover{background:rgba(255,255,255,0.06);}' + '.ck-divider{width:1px;background:rgba(0,0,0,0.12);flex-shrink:0;}' + ':root.dark .ck-divider{background:rgba(255,255,255,0.12);}' + '.ck-dd{position:absolute;top:calc(100% + 4px);right:0;min-width:180px;background:#fff;border:1px solid rgba(0,0,0,0.12);border-radius:8px;box-shadow:0 4px 12px rgba(0,0,0,0.1);padding:4px;display:none;z-index:200;}' + ':root.dark .ck-dd{background:#1f2937;border-color:rgba(255,255,255,0.1);box-shadow:0 4px 16px rgba(0,0,0,0.35);}' + '.ck-item{display:block;width:100%;padding:7px 12px;border:none;background:none;border-radius:6px;cursor:pointer;font-size:13px;font-family:inherit;text-align:left;color:#374151;}' + ':root.dark .ck-item{color:#d1d5db;}' + '.ck-item:hover{background:rgba(0,0,0,0.05);}' + ':root.dark .ck-item:hover{background:rgba(255,255,255,0.07);}';
        document.head.appendChild(s);
      }
      var wrap = document.createElement('div');
      wrap.id = 'ck-tools';
      var row = document.createElement('div');
      row.className = 'ck-row';
      var mainBtn = document.createElement('button');
      mainBtn.className = 'ck-btn';
      mainBtn.textContent = 'Copy page';
      var divider = document.createElement('span');
      divider.className = 'ck-divider';
      var chevron = document.createElement('button');
      chevron.className = 'ck-chevron';
      chevron.textContent = '▾';
      var dd = document.createElement('div');
      dd.className = 'ck-dd';
      function closeDD() {
        dd.style.display = 'none';
      }
      function openDD() {
        dd.style.display = 'block';
      }
      chevron.onclick = function (e) {
        e.stopPropagation();
        if (dd.style.display === 'block') {
          closeDD();
        } else {
          openDD();
        }
      };
      document.addEventListener('click', function (e) {
        if (!e.target.closest('#ck-tools')) {
          closeDD();
        }
      });
      document.addEventListener('keydown', function (e) {
        if (e.key === 'Escape') {
          closeDD();
        }
      });
      function makeItem(label, fn) {
        var b = document.createElement('button');
        b.className = 'ck-item';
        b.textContent = label;
        b.onclick = function () {
          fn();
          closeDD();
        };
        return b;
      }
      function getMarkdown() {
        var walk = function (node) {
          if (!node) return '';
          if (node.nodeType === 3) return node.textContent || '';
          if (node.nodeType !== 1) return '';
          var tag = node.tagName.toLowerCase();
          var skip = ['script', 'style', 'svg', 'noscript', 'button', 'iframe'];
          if (skip.indexOf(tag) !== -1) return '';
          if (node.id === 'ck-tools') return '';
          var ch = Array.from(node.childNodes).map(walk).join('');
          if (tag === 'h1') return '\n# ' + ch.trim() + '\n\n';
          if (tag === 'h2') return '\n## ' + ch.trim() + '\n\n';
          if (tag === 'h3') return '\n### ' + ch.trim() + '\n\n';
          if (tag === 'p') return '\n' + ch.trim() + '\n\n';
          if (tag === 'pre') return '\n```\n' + node.textContent.trim() + '\n```\n\n';
          if (tag === 'li') return '- ' + ch.trim() + '\n';
          if (tag === 'code') return '`' + ch.trim() + '`';
          return ch;
        };
        var content = document.querySelector('.mdx-content') || document.getElementById('content-area') || document.body;
        return walk(content).replace(/\n\n\n+/g, '\n\n').trim();
      }
      function copyMd() {
        var md = getMarkdown();
        navigator.clipboard.writeText(md).then(function () {
          mainBtn.textContent = 'Copied!';
          setTimeout(function () {
            mainBtn.textContent = 'Copy page';
          }, 2000);
        });
      }
      function viewMd() {
        var md = getMarkdown();
        var safe = md.split('&').join('&amp;').split('<').join('&lt;').split('>').join('&gt;');
        var html = '<!DOCTYPE html><html><head><meta charset="utf-8"><style>body{font-family:monospace;max-width:860px;margin:40px auto;padding:0 24px;line-height:1.7;white-space:pre-wrap;word-wrap:break-word}</style></head><body>' + safe + '</body></html>';
        window.open(URL.createObjectURL(new Blob([html], {
          type: 'text/html'
        })), '_blank');
      }
      function openClaude() {
        var prompt = 'Can you read this Cekura docs page ' + window.location.href + ' so I can ask you questions?';
        window.open('https://claude.ai/new?q=' + encodeURIComponent(prompt), '_blank');
      }
      mainBtn.onclick = copyMd;
      dd.appendChild(makeItem('Copy page', copyMd));
      dd.appendChild(makeItem('View as Markdown', viewMd));
      dd.appendChild(makeItem('Open in Claude', openClaude));
      row.appendChild(mainBtn);
      row.appendChild(divider);
      row.appendChild(chevron);
      wrap.appendChild(row);
      wrap.appendChild(dd);
      anchor.style.position = 'relative';
      anchor.insertBefore(wrap, anchor.firstChild);
    }, 50);
  }
  return null;
};

<CopyPageButton />

## Overview

Use this flow to test your agent by joining a LiveKit room via WebRTC for each evaluator run. Each item you submit will create a run that connects using your provided LiveKit room URL and access token. This page covers both **voice** and **chat (text mode)** flows.

<Note>
  Each array element you send creates a separate run. To run the same scenario multiple times in parallel, repeat that scenario object multiple times in the payload.
</Note>

## Prerequisites

* A Cekura account
* One or more scenarios created for your agent
* LiveKit room URL and a valid access token for each run you intend to start

## API Endpoint

* **Method**: POST
* **URL**: `https://api.cekura.ai/test_framework/v1/scenarios-external/run_scenarios_livekit/`
* **Headers**:
  * `X-CEKURA-API-KEY`: your API key
  * `Content-Type`: `application/json`

### Request Body

* **scenarios**: Array of objects. Each object fields:
  * **scenario** (number, required): Scenario ID
  * **livekit\_room\_url** (string, required): Your LiveKit room URL
  * **access\_token** (string, required): LiveKit access token

### Example: Minimal Single Run (cURL)

```bash theme={null}
curl -X POST \
  'https://api.cekura.ai/test_framework/v1/scenarios-external/run_scenarios_livekit/' \
  -H 'X-CEKURA-API-KEY: <YOUR_API_KEY>' \
  -H 'Content-Type: application/json' \
  -d '{
    "scenarios": [
      {
        "scenario": 30,
        "livekit_room_url": "wss://<LK_ROOM_URL>",
        "access_token": "<LK_TOKEN>"
      }
    ]
  }'
```

### Example: Multiple Runs (JSON)

```json theme={null}
{
  "scenarios": [
    {
      "scenario": 30,
      "livekit_room_url": "wss://<LK_ROOM_URL>",
      "access_token": "<LK_TOKEN_1>"
    },
    {
      "scenario": 31,
      "livekit_room_url": "wss://<LK_ROOM_URL>",
      "access_token": "<LK_TOKEN_2>"
    }
  ]
}
```

### Example: Python

```python theme={null}
import requests

API_KEY = "<YOUR_API_KEY>"
BASE_URL = "https://api.cekura.ai/test_framework"

headers = {
    "X-CEKURA-API-KEY": API_KEY,
    "Content-Type": "application/json",
}

payload = {
    "scenarios": [
        {
            "scenario": 30,
            "livekit_room_url": "wss://<LK_ROOM_URL>",
            "access_token": "<LK_TOKEN_1>",
        },
        {
            "scenario": 31,
            "livekit_room_url": "wss://<LK_ROOM_URL>",
            "access_token": "<LK_TOKEN_2>",
        },
    ]
}

resp = requests.post(
    f"{BASE_URL}/v1/scenarios-external/run_scenarios_livekit/",
    headers=headers,
    json=payload,
)
result = resp.json()
print(result)
```

## Expected Behavior

* A result is created and a run is queued for each item in `scenarios`
* Each run connects to the provided LiveKit room URL using the given access token
* You can poll run statuses using the Bulk Runs API:

<Card title="API Reference" icon="link">
  Get Runs with IDs: [API Doc](/api-reference/test_framework/list-runs-with-ids)
</Card>

***

## Chat Flow (Text Mode)

Use this endpoint to test your LiveKit agent using text-based chat instead of voice. The same manual approach applies (you provide room URL and token), but the evaluator communicates via data messages rather than audio.

### Dispatching Your Agent for Chat Mode

If you have [tracing](/documentation/integrations/livekit/tracing) enabled, you can send `text_mode: true` in the agent dispatch metadata and the tracer will handle text mode automatically:

```python theme={null}
from livekit.api import LiveKitAPI, CreateAgentDispatchRequest
import json

livekit_api = LiveKitAPI(
    url="wss://<YOUR_LIVEKIT_URL>",
    api_key="<YOUR_API_KEY>",
    api_secret="<YOUR_API_SECRET>",
)

dispatch_metadata = {
    "text_mode": True,
    # ... any other metadata your agent needs
}

await livekit_api.agent_dispatch.create_dispatch(
    CreateAgentDispatchRequest(
        agent_name="your-agent-name",
        room="your-room-name",
        metadata=json.dumps(dispatch_metadata),
    )
)
```

<Note>
  If you do not have tracing enabled, you must ensure your agent can operate in an LLM-only mode (without STT/TTS) and handle text-based data messages directly.
</Note>

### API Endpoint

* **Method**: POST
* **URL**: `https://api.cekura.ai/test_framework/v1/scenarios-external/run_scenarios_livekit_chat/`
* **Headers**:
  * `X-CEKURA-API-KEY`: your API key
  * `Content-Type`: `application/json`

### Request Body

* **scenarios**: Array of objects. Each object fields:
  * **scenario** (number, required): Scenario ID
  * **livekit\_room\_url** (string, required): Your LiveKit room URL
  * **access\_token** (string, required): LiveKit access token
  * **publish\_data\_message** (object, optional): Additional data message payload
* **agent** (number, optional): Agent ID. Not required if scenarios already have agents assigned.

### Example: cURL

```bash theme={null}
curl -X POST \
  'https://api.cekura.ai/test_framework/v1/scenarios-external/run_scenarios_livekit_chat/' \
  -H 'X-CEKURA-API-KEY: <YOUR_API_KEY>' \
  -H 'Content-Type: application/json' \
  -d '{
    "scenarios": [
      {
        "scenario": 30,
        "livekit_room_url": "wss://<LK_ROOM_URL>",
        "access_token": "<LK_TOKEN>"
      }
    ]
  }'
```

### Example: Python

```python theme={null}
import requests

API_KEY = "<YOUR_API_KEY>"
BASE_URL = "https://api.cekura.ai/test_framework"

headers = {
    "X-CEKURA-API-KEY": API_KEY,
    "Content-Type": "application/json",
}

payload = {
    "scenarios": [
        {
            "scenario": 30,
            "livekit_room_url": "wss://<LK_ROOM_URL>",
            "access_token": "<LK_TOKEN>",
        },
    ],
}

resp = requests.post(
    f"{BASE_URL}/v1/scenarios-external/run_scenarios_livekit_chat/",
    headers=headers,
    json=payload,
)
result = resp.json()
print(result)
```

### Expected Behavior

* A result is created and a run is queued for each item in `scenarios`
* Each run connects to the provided LiveKit room and communicates via text data messages
* The webhook flow works the same as voice — Cekura matches the webhook transcript to the run using similarity matching and temporal alignment

### Billing

<Info>
  Chat runs are billed at **text simulation rates**, not voice simulation rates.
</Info>

## Troubleshooting

* **401/403 errors**: Check your `X-CEKURA-API-KEY`
* **Room connection failures**: Verify `livekit_room_url` and `access_token`
* **No runs created**: Ensure the `scenarios` array is not empty and scenario IDs are valid
* **Chat messages not received**: Ensure your agent was dispatched with `text_mode: true` in dispatch metadata

For detailed LiveKit traces and observability, check out the [LiveKit Tracing documentation](/documentation/integrations/livekit/tracing).

## Next Steps

* Learn about [custom metrics](/documentation/key-concepts/metrics/custom-metrics)
* Explore [predefined metrics](/documentation/key-concepts/metrics/pre-defined-metrics)
* Set up [instruction following metric](/documentation/key-concepts/metrics/instruction-following-metric)
* Perform [load testing](/documentation/guides/testing-agents/load-testing)
