---
title: "Run scripted tests against a Speak AI voice agent"
description: "Define scenarios for a voice agent, start and control test runs, read run history and score trends, and apply a run's recommendations back to the agent."
---

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

# Run scripted tests against a Speak AI voice agent


import EndpointIndex from "@/components/api/EndpointIndex.astro";

The Speak AI API exposes 13 testing endpoints under the base URL `https://api.speakai.co/v1`. Every request needs the `x-speakai-key` and `x-access-token` headers described in [Authentication](/api/authentication/).

Scripted test scenarios and run history for a voice agent. The run lifecycle (queue/pause/resume/cancel) is live; the execution engine that actually drives a simulated conversation is not wired up yet, so a run stays queued until that lands.

## What can you do with the testing endpoints?

Speak AI groups these 13 endpoints under the testing resource. Each entry below links to the full reference for that endpoint further down this page.

### Runs

| Method | Path | What it does |
| --- | --- | --- |
| `GET` | [`/voice/testing/{agentId}/runs`](#get-voice-testing-agent-id-runs) | List Test Runs |
| `POST` | [`/voice/testing/{agentId}/run`](#post-voice-testing-agent-id-run) | Start Test Run |
| `GET` | [`/voice/testing/{agentId}/runs/{runId}`](#get-voice-testing-agent-id-runs-run-id) | Get Test Run Detail |
| `POST` | [`/voice/testing/{agentId}/run/{runId}/cancel`](#post-voice-testing-agent-id-run-run-id-cancel) | Cancel Test Run |
| `POST` | [`/voice/testing/{agentId}/run/{runId}/pause`](#post-voice-testing-agent-id-run-run-id-pause) | Pause Test Run |
| `POST` | [`/voice/testing/{agentId}/run/{runId}/resume`](#post-voice-testing-agent-id-run-run-id-resume) | Resume Test Run |
| `GET` | [`/voice/testing/{agentId}/run/active`](#get-voice-testing-agent-id-run-active) | Get Active Test Run |
| `POST` | [`/voice/testing/{agentId}/runs/{runId}/recommendations/{recId}/apply`](#post-voice-testing-agent-id-runs-run-id-recommendations-rec-id-apply) | Apply Test Run Recommendation |

### Suite

| Method | Path | What it does |
| --- | --- | --- |
| `GET` | [`/voice/testing/{agentId}/suite`](#get-voice-testing-agent-id-suite) | Get Test Suite |
| `PUT` | [`/voice/testing/{agentId}/suite`](#put-voice-testing-agent-id-suite) | Create Or Update Test Suite |
| `POST` | [`/voice/testing/{agentId}/suite/generate`](#post-voice-testing-agent-id-suite-generate) | Generate Default Test Suite |

### Results

| Method | Path | What it does |
| --- | --- | --- |
| `GET` | [`/voice/testing/{agentId}/baseline`](#get-voice-testing-agent-id-baseline) | Get Test Baseline |
| `GET` | [`/voice/testing/{agentId}/score-history`](#get-voice-testing-agent-id-score-history) | Get Test Score History |

## Runs

<h2 id="get-voice-testing-agent-id-runs">List Test Runs</h2>

#### List Runs

Most recent first, capped at 100 regardless of the `limit` query parameter.

### Parameters

| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `agentId` | path | string | Yes |  |
| `limit` | query | integer | No | Defaults to 20. Values above 100 are capped at 100; non-numeric values fall back to the default. |

<div slot="code">

<CodePanel label="Example request">

```bash
curl -X GET 'https://api.speakai.co/v1/voice/testing/AGENT_ID/runs' \
  -H 'x-speakai-key: sk_test_speak_0000000000000000' \
  -H 'x-access-token: eyJhbGciOiJIUzI1NiJ9.test-access-token.0000000000'
```

</CodePanel>

<ResponsePanel id="get-voice-testing-agent-id-runs" statuses={[{"code":"200","description":"OK"},{"code":"404","description":"Agent not found"}]}>

<div slot="status-200">

**`200` OK**

Response body, `application/json`.

| Field | Type | Description |
| --- | --- | --- |
| `status` | string | One of: `success` . |
| `data` | object |  |
| `data.runs` | object[] |  |

<details>
<summary>`data.runs` -- 22 more fields</summary>

| Field | Type | Description |
| --- | --- | --- |
| `_id` | string | MongoDB document id, included because every document has one; use runId to identify it. |
| `runId` | string |  |
| `suiteId` | string |  |
| `agentId` | string |  |
| `companyId` | string |  |
| `status` | string | One of: `queued`, `running`, `paused`, `completed`, `failed`, `cancelled`, `budget_exceeded` . |
| `trigger` | string | One of: `manual`, `kb_update`, `instruction_save`, `scheduled` . |
| `currentScenarioIndex` | integer |  |
| `totalScenarios` | integer |  |
| `scenarioResults` | object[] | Populated as the execution engine appends results. Empty on a run that never left queued. |
| `overallScore` | number |  |
| `passedCount` | integer |  |
| `failedCount` | integer |  |
| `totalCount` | integer |  |
| `totalCost` | number |  |
| `isRegression` | boolean |  |
| `baselineScore` | number |  |
| `scoreDelta` | number |  |
| `recommendations` | object[] |  |
| `startedAt` | string (date-time) |  |
| `completedAt` | string (date-time) |  |
| `failureReason` | string |  |

</details>

</div>

<div slot="status-404">

**`404` Agent not found**

Response body, `application/json`.

| Field | Type | Description |
| --- | --- | --- |
| `status` | string | One of: `failed` . |
| `requestId` | string |  |
| `code` | integer |  |
| `message` | string |  |
| `hints` | string[] |  |

Example response, `application/json`.

```json
{
  "status": "failed",
  "code": 404,
  "message": "Agent not found",
  "hints": []
}
```

</div>

</ResponsePanel>

</div>

<h2 id="post-voice-testing-agent-id-run">Start Test Run</h2>

#### Start Test Run

Requires the OWNER or ADMIN role. **The live execution engine is not wired up yet.** These endpoints create and transition the run record, `queued`, `running`, `paused`, `completed`, `cancelled`, `budget_exceeded`, but nothing here drives a simulated conversation against the agent. A run created with `POST /run` stays `queued` until something else (not yet built) advances it; `scenarioResults` stays empty and `overallScore` stays 0.

Rejects with 409 if the agent has no test suite, the suite has no enabled scenarios, or an active run (`queued`/`running`/`paused`) already exists for this agent, only one run can be active per agent at a time.

### Parameters

| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `agentId` | path | string | Yes |  |

<div slot="code">

<CodePanel label="Example request">

```bash
curl -X POST 'https://api.speakai.co/v1/voice/testing/AGENT_ID/run' \
  -H 'x-speakai-key: sk_test_speak_0000000000000000' \
  -H 'x-access-token: eyJhbGciOiJIUzI1NiJ9.test-access-token.0000000000'
```

</CodePanel>

<ResponsePanel id="post-voice-testing-agent-id-run" statuses={[{"code":"201","description":"Created"},{"code":"403","description":"Caller's role is not OWNER or ADMIN"},{"code":"404","description":"Agent, run, or recommendation not found"},{"code":"409","description":"Conflict, no test suite configured, no enabled scenarios, an active run already exists, or the transition is not valid from the run's current status"}]}>

<div slot="status-201">

**`201` Created**

Response body, `application/json`.

| Field | Type | Description |
| --- | --- | --- |
| `status` | string | One of: `success` . |
| `data` | object |  |
| `data.run` | object |  |

<details>
<summary>`data.run` -- 22 more fields</summary>

| Field | Type | Description |
| --- | --- | --- |
| `_id` | string | MongoDB document id, included because every document has one; use runId to identify it. |
| `runId` | string |  |
| `suiteId` | string |  |
| `agentId` | string |  |
| `companyId` | string |  |
| `status` | string | One of: `queued`, `running`, `paused`, `completed`, `failed`, `cancelled`, `budget_exceeded` . |
| `trigger` | string | One of: `manual`, `kb_update`, `instruction_save`, `scheduled` . |
| `currentScenarioIndex` | integer |  |
| `totalScenarios` | integer |  |
| `scenarioResults` | object[] | Populated as the execution engine appends results. Empty on a run that never left queued. |
| `overallScore` | number |  |
| `passedCount` | integer |  |
| `failedCount` | integer |  |
| `totalCount` | integer |  |
| `totalCost` | number |  |
| `isRegression` | boolean |  |
| `baselineScore` | number |  |
| `scoreDelta` | number |  |
| `recommendations` | object[] |  |
| `startedAt` | string (date-time) |  |
| `completedAt` | string (date-time) |  |
| `failureReason` | string |  |

</details>

</div>

<div slot="status-403">

**`403` Caller's role is not OWNER or ADMIN**

Response body, `application/json`.

| Field | Type | Description |
| --- | --- | --- |
| `status` | string | One of: `failed` . |
| `message` | string |  |

Example response, `application/json`.

```json
{
  "status": "failed",
  "message": "You do not have permission to manage agents"
}
```

</div>

<div slot="status-404">

**`404` Agent, run, or recommendation not found**

Response body, `application/json`.

| Field | Type | Description |
| --- | --- | --- |
| `status` | string | One of: `failed` . |
| `requestId` | string |  |
| `code` | integer |  |
| `message` | string |  |
| `hints` | string[] |  |

Example response, `application/json`.

```json
{
  "status": "failed",
  "code": 404,
  "message": "Agent, run, or recommendation not found",
  "hints": []
}
```

</div>

<div slot="status-409">

**`409` Conflict, no test suite configured, no enabled scenarios, an active run already exists, or the transition is not valid from the run's current status**

Response body, `application/json`.

| Field | Type | Description |
| --- | --- | --- |
| `status` | string | One of: `failed` . |
| `message` | string |  |

</div>

</ResponsePanel>

</div>

<h2 id="get-voice-testing-agent-id-runs-run-id">Get Test Run Detail</h2>

#### Get Run Detail

Full run record including `scenarioResults` and `recommendations`, scoped to your company. The `agentId` path segment is not used to filter this lookup, only `runId` and your company.

### Parameters

| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `agentId` | path | string | Yes |  |
| `runId` | path | string | Yes |  |

<div slot="code">

<CodePanel label="Example request">

```bash
curl -X GET 'https://api.speakai.co/v1/voice/testing/AGENT_ID/runs/RUN_ID' \
  -H 'x-speakai-key: sk_test_speak_0000000000000000' \
  -H 'x-access-token: eyJhbGciOiJIUzI1NiJ9.test-access-token.0000000000'
```

</CodePanel>

<ResponsePanel id="get-voice-testing-agent-id-runs-run-id" statuses={[{"code":"200","description":"OK"},{"code":"404","description":"Run not found"}]}>

<div slot="status-200">

**`200` OK**

Response body, `application/json`.

| Field | Type | Description |
| --- | --- | --- |
| `status` | string | One of: `success` . |
| `data` | object |  |
| `data.run` | object |  |

<details>
<summary>`data.run` -- 22 more fields</summary>

| Field | Type | Description |
| --- | --- | --- |
| `_id` | string | MongoDB document id, included because every document has one; use runId to identify it. |
| `runId` | string |  |
| `suiteId` | string |  |
| `agentId` | string |  |
| `companyId` | string |  |
| `status` | string | One of: `queued`, `running`, `paused`, `completed`, `failed`, `cancelled`, `budget_exceeded` . |
| `trigger` | string | One of: `manual`, `kb_update`, `instruction_save`, `scheduled` . |
| `currentScenarioIndex` | integer |  |
| `totalScenarios` | integer |  |
| `scenarioResults` | object[] | Populated as the execution engine appends results. Empty on a run that never left queued. |
| `overallScore` | number |  |
| `passedCount` | integer |  |
| `failedCount` | integer |  |
| `totalCount` | integer |  |
| `totalCost` | number |  |
| `isRegression` | boolean |  |
| `baselineScore` | number |  |
| `scoreDelta` | number |  |
| `recommendations` | object[] |  |
| `startedAt` | string (date-time) |  |
| `completedAt` | string (date-time) |  |
| `failureReason` | string |  |

</details>

</div>

<div slot="status-404">

**`404` Run not found**

Response body, `application/json`.

| Field | Type | Description |
| --- | --- | --- |
| `status` | string | One of: `failed` . |
| `requestId` | string |  |
| `code` | integer |  |
| `message` | string |  |
| `hints` | string[] |  |

Example response, `application/json`.

```json
{
  "status": "failed",
  "code": 404,
  "message": "Run not found",
  "hints": []
}
```

</div>

</ResponsePanel>

</div>

<h2 id="post-voice-testing-agent-id-run-run-id-cancel">Cancel Test Run</h2>

#### Cancel Run

Requires the OWNER or ADMIN role. Valid from `queued`, `running`, or `paused`. Terminal, a cancelled run can never be resumed.

### Parameters

| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `agentId` | path | string | Yes |  |
| `runId` | path | string | Yes |  |

<div slot="code">

<CodePanel label="Example request">

```bash
curl -X POST 'https://api.speakai.co/v1/voice/testing/AGENT_ID/run/RUN_ID/cancel' \
  -H 'x-speakai-key: sk_test_speak_0000000000000000' \
  -H 'x-access-token: eyJhbGciOiJIUzI1NiJ9.test-access-token.0000000000'
```

</CodePanel>

<ResponsePanel id="post-voice-testing-agent-id-run-run-id-cancel" statuses={[{"code":"200","description":"OK"},{"code":"403","description":"Caller's role is not OWNER or ADMIN"},{"code":"404","description":"Agent, run, or recommendation not found"},{"code":"409","description":"Conflict, no test suite configured, no enabled scenarios, an active run already exists, or the transition is not valid from the run's current status"}]}>

<div slot="status-200">

**`200` OK**

Response body, `application/json`.

| Field | Type | Description |
| --- | --- | --- |
| `status` | string | One of: `success` . |
| `data` | object |  |
| `data.run` | object |  |

<details>
<summary>`data.run` -- 22 more fields</summary>

| Field | Type | Description |
| --- | --- | --- |
| `_id` | string | MongoDB document id, included because every document has one; use runId to identify it. |
| `runId` | string |  |
| `suiteId` | string |  |
| `agentId` | string |  |
| `companyId` | string |  |
| `status` | string | One of: `queued`, `running`, `paused`, `completed`, `failed`, `cancelled`, `budget_exceeded` . |
| `trigger` | string | One of: `manual`, `kb_update`, `instruction_save`, `scheduled` . |
| `currentScenarioIndex` | integer |  |
| `totalScenarios` | integer |  |
| `scenarioResults` | object[] | Populated as the execution engine appends results. Empty on a run that never left queued. |
| `overallScore` | number |  |
| `passedCount` | integer |  |
| `failedCount` | integer |  |
| `totalCount` | integer |  |
| `totalCost` | number |  |
| `isRegression` | boolean |  |
| `baselineScore` | number |  |
| `scoreDelta` | number |  |
| `recommendations` | object[] |  |
| `startedAt` | string (date-time) |  |
| `completedAt` | string (date-time) |  |
| `failureReason` | string |  |

</details>

</div>

<div slot="status-403">

**`403` Caller's role is not OWNER or ADMIN**

Response body, `application/json`.

| Field | Type | Description |
| --- | --- | --- |
| `status` | string | One of: `failed` . |
| `message` | string |  |

Example response, `application/json`.

```json
{
  "status": "failed",
  "message": "You do not have permission to manage agents"
}
```

</div>

<div slot="status-404">

**`404` Agent, run, or recommendation not found**

Response body, `application/json`.

| Field | Type | Description |
| --- | --- | --- |
| `status` | string | One of: `failed` . |
| `requestId` | string |  |
| `code` | integer |  |
| `message` | string |  |
| `hints` | string[] |  |

Example response, `application/json`.

```json
{
  "status": "failed",
  "code": 404,
  "message": "Agent, run, or recommendation not found",
  "hints": []
}
```

</div>

<div slot="status-409">

**`409` Conflict, no test suite configured, no enabled scenarios, an active run already exists, or the transition is not valid from the run's current status**

Response body, `application/json`.

| Field | Type | Description |
| --- | --- | --- |
| `status` | string | One of: `failed` . |
| `message` | string |  |

</div>

</ResponsePanel>

</div>

<h2 id="post-voice-testing-agent-id-run-run-id-pause">Pause Test Run</h2>

#### Pause Run

Requires the OWNER or ADMIN role. Valid only from `queued` or `running`. **The live execution engine is not wired up yet.** These endpoints create and transition the run record, `queued`, `running`, `paused`, `completed`, `cancelled`, `budget_exceeded`, but nothing here drives a simulated conversation against the agent. A run created with `POST /run` stays `queued` until something else (not yet built) advances it; `scenarioResults` stays empty and `overallScore` stays 0.

### Parameters

| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `agentId` | path | string | Yes |  |
| `runId` | path | string | Yes |  |

<div slot="code">

<CodePanel label="Example request">

```bash
curl -X POST 'https://api.speakai.co/v1/voice/testing/AGENT_ID/run/RUN_ID/pause' \
  -H 'x-speakai-key: sk_test_speak_0000000000000000' \
  -H 'x-access-token: eyJhbGciOiJIUzI1NiJ9.test-access-token.0000000000'
```

</CodePanel>

<ResponsePanel id="post-voice-testing-agent-id-run-run-id-pause" statuses={[{"code":"200","description":"OK"},{"code":"403","description":"Caller's role is not OWNER or ADMIN"},{"code":"404","description":"Agent, run, or recommendation not found"},{"code":"409","description":"Conflict, no test suite configured, no enabled scenarios, an active run already exists, or the transition is not valid from the run's current status"}]}>

<div slot="status-200">

**`200` OK**

Response body, `application/json`.

| Field | Type | Description |
| --- | --- | --- |
| `status` | string | One of: `success` . |
| `data` | object |  |
| `data.run` | object |  |

<details>
<summary>`data.run` -- 22 more fields</summary>

| Field | Type | Description |
| --- | --- | --- |
| `_id` | string | MongoDB document id, included because every document has one; use runId to identify it. |
| `runId` | string |  |
| `suiteId` | string |  |
| `agentId` | string |  |
| `companyId` | string |  |
| `status` | string | One of: `queued`, `running`, `paused`, `completed`, `failed`, `cancelled`, `budget_exceeded` . |
| `trigger` | string | One of: `manual`, `kb_update`, `instruction_save`, `scheduled` . |
| `currentScenarioIndex` | integer |  |
| `totalScenarios` | integer |  |
| `scenarioResults` | object[] | Populated as the execution engine appends results. Empty on a run that never left queued. |
| `overallScore` | number |  |
| `passedCount` | integer |  |
| `failedCount` | integer |  |
| `totalCount` | integer |  |
| `totalCost` | number |  |
| `isRegression` | boolean |  |
| `baselineScore` | number |  |
| `scoreDelta` | number |  |
| `recommendations` | object[] |  |
| `startedAt` | string (date-time) |  |
| `completedAt` | string (date-time) |  |
| `failureReason` | string |  |

</details>

</div>

<div slot="status-403">

**`403` Caller's role is not OWNER or ADMIN**

Response body, `application/json`.

| Field | Type | Description |
| --- | --- | --- |
| `status` | string | One of: `failed` . |
| `message` | string |  |

Example response, `application/json`.

```json
{
  "status": "failed",
  "message": "You do not have permission to manage agents"
}
```

</div>

<div slot="status-404">

**`404` Agent, run, or recommendation not found**

Response body, `application/json`.

| Field | Type | Description |
| --- | --- | --- |
| `status` | string | One of: `failed` . |
| `requestId` | string |  |
| `code` | integer |  |
| `message` | string |  |
| `hints` | string[] |  |

Example response, `application/json`.

```json
{
  "status": "failed",
  "code": 404,
  "message": "Agent, run, or recommendation not found",
  "hints": []
}
```

</div>

<div slot="status-409">

**`409` Conflict, no test suite configured, no enabled scenarios, an active run already exists, or the transition is not valid from the run's current status**

Response body, `application/json`.

| Field | Type | Description |
| --- | --- | --- |
| `status` | string | One of: `failed` . |
| `message` | string |  |

</div>

</ResponsePanel>

</div>

<h2 id="post-voice-testing-agent-id-run-run-id-resume">Resume Test Run</h2>

#### Resume Run

Requires the OWNER or ADMIN role. Valid only from `paused`, transitioning back to `running`.

### Parameters

| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `agentId` | path | string | Yes |  |
| `runId` | path | string | Yes |  |

<div slot="code">

<CodePanel label="Example request">

```bash
curl -X POST 'https://api.speakai.co/v1/voice/testing/AGENT_ID/run/RUN_ID/resume' \
  -H 'x-speakai-key: sk_test_speak_0000000000000000' \
  -H 'x-access-token: eyJhbGciOiJIUzI1NiJ9.test-access-token.0000000000'
```

</CodePanel>

<ResponsePanel id="post-voice-testing-agent-id-run-run-id-resume" statuses={[{"code":"200","description":"OK"},{"code":"403","description":"Caller's role is not OWNER or ADMIN"},{"code":"404","description":"Agent, run, or recommendation not found"},{"code":"409","description":"Conflict, no test suite configured, no enabled scenarios, an active run already exists, or the transition is not valid from the run's current status"}]}>

<div slot="status-200">

**`200` OK**

Response body, `application/json`.

| Field | Type | Description |
| --- | --- | --- |
| `status` | string | One of: `success` . |
| `data` | object |  |
| `data.run` | object |  |

<details>
<summary>`data.run` -- 22 more fields</summary>

| Field | Type | Description |
| --- | --- | --- |
| `_id` | string | MongoDB document id, included because every document has one; use runId to identify it. |
| `runId` | string |  |
| `suiteId` | string |  |
| `agentId` | string |  |
| `companyId` | string |  |
| `status` | string | One of: `queued`, `running`, `paused`, `completed`, `failed`, `cancelled`, `budget_exceeded` . |
| `trigger` | string | One of: `manual`, `kb_update`, `instruction_save`, `scheduled` . |
| `currentScenarioIndex` | integer |  |
| `totalScenarios` | integer |  |
| `scenarioResults` | object[] | Populated as the execution engine appends results. Empty on a run that never left queued. |
| `overallScore` | number |  |
| `passedCount` | integer |  |
| `failedCount` | integer |  |
| `totalCount` | integer |  |
| `totalCost` | number |  |
| `isRegression` | boolean |  |
| `baselineScore` | number |  |
| `scoreDelta` | number |  |
| `recommendations` | object[] |  |
| `startedAt` | string (date-time) |  |
| `completedAt` | string (date-time) |  |
| `failureReason` | string |  |

</details>

</div>

<div slot="status-403">

**`403` Caller's role is not OWNER or ADMIN**

Response body, `application/json`.

| Field | Type | Description |
| --- | --- | --- |
| `status` | string | One of: `failed` . |
| `message` | string |  |

Example response, `application/json`.

```json
{
  "status": "failed",
  "message": "You do not have permission to manage agents"
}
```

</div>

<div slot="status-404">

**`404` Agent, run, or recommendation not found**

Response body, `application/json`.

| Field | Type | Description |
| --- | --- | --- |
| `status` | string | One of: `failed` . |
| `requestId` | string |  |
| `code` | integer |  |
| `message` | string |  |
| `hints` | string[] |  |

Example response, `application/json`.

```json
{
  "status": "failed",
  "code": 404,
  "message": "Agent, run, or recommendation not found",
  "hints": []
}
```

</div>

<div slot="status-409">

**`409` Conflict, no test suite configured, no enabled scenarios, an active run already exists, or the transition is not valid from the run's current status**

Response body, `application/json`.

| Field | Type | Description |
| --- | --- | --- |
| `status` | string | One of: `failed` . |
| `message` | string |  |

</div>

</ResponsePanel>

</div>

<h2 id="get-voice-testing-agent-id-run-active">Get Active Test Run</h2>

#### Get Active Run

Returns the agent's currently active run (`queued`, `running`, or `paused`), or `null` in `data.run` if none is active. Not a 404 when there is no active run.

### Parameters

| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `agentId` | path | string | Yes |  |

<div slot="code">

<CodePanel label="Example request">

```bash
curl -X GET 'https://api.speakai.co/v1/voice/testing/AGENT_ID/run/active' \
  -H 'x-speakai-key: sk_test_speak_0000000000000000' \
  -H 'x-access-token: eyJhbGciOiJIUzI1NiJ9.test-access-token.0000000000'
```

</CodePanel>

<ResponsePanel id="get-voice-testing-agent-id-run-active" statuses={[{"code":"200","description":"OK"},{"code":"404","description":"Agent not found"}]}>

<div slot="status-200">

**`200` OK**

Response body, `application/json`.

| Field | Type | Description |
| --- | --- | --- |
| `status` | string | One of: `success` . |
| `data` | object |  |
| `data.run` | object |  |

<details>
<summary>`data.run` -- 22 more fields</summary>

| Field | Type | Description |
| --- | --- | --- |
| `_id` | string | MongoDB document id, included because every document has one; use runId to identify it. |
| `runId` | string |  |
| `suiteId` | string |  |
| `agentId` | string |  |
| `companyId` | string |  |
| `status` | string | One of: `queued`, `running`, `paused`, `completed`, `failed`, `cancelled`, `budget_exceeded` . |
| `trigger` | string | One of: `manual`, `kb_update`, `instruction_save`, `scheduled` . |
| `currentScenarioIndex` | integer |  |
| `totalScenarios` | integer |  |
| `scenarioResults` | object[] | Populated as the execution engine appends results. Empty on a run that never left queued. |
| `overallScore` | number |  |
| `passedCount` | integer |  |
| `failedCount` | integer |  |
| `totalCount` | integer |  |
| `totalCost` | number |  |
| `isRegression` | boolean |  |
| `baselineScore` | number |  |
| `scoreDelta` | number |  |
| `recommendations` | object[] |  |
| `startedAt` | string (date-time) |  |
| `completedAt` | string (date-time) |  |
| `failureReason` | string |  |

</details>

</div>

<div slot="status-404">

**`404` Agent not found**

Response body, `application/json`.

| Field | Type | Description |
| --- | --- | --- |
| `status` | string | One of: `failed` . |
| `requestId` | string |  |
| `code` | integer |  |
| `message` | string |  |
| `hints` | string[] |  |

Example response, `application/json`.

```json
{
  "status": "failed",
  "code": 404,
  "message": "Agent not found",
  "hints": []
}
```

</div>

</ResponsePanel>

</div>

<h2 id="post-voice-testing-agent-id-runs-run-id-recommendations-rec-id-apply">Apply Test Run Recommendation</h2>

#### Apply Recommendation

Requires the OWNER or ADMIN role. Applies a recommendation's quick action to the agent:

- `patch_instructions` / `instructions`, appends the recommendation's suggested fix to the agent's instructions.
- `update_personality` / `personality`, appends it to the agent's personality.
- `add_topic_to_avoid` / `topics_to_avoid`, adds it to `chatSettings.topicsToAvoid`.
- `add_lexicon_term` / `lexicon` / `add_to_kb` / `kb`, **marked applied but makes no change to the agent.** These two categories are not wired up yet; the run record reflects the acknowledgement, the agent config does not.

Any other action type returns 400. A recommendation with no `suggestedFix` also returns 400.

### Parameters

| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `agentId` | path | string | Yes |  |
| `runId` | path | string | Yes |  |
| `recId` | path | string | Yes |  |

<div slot="code">

<CodePanel label="Example request">

```bash
curl -X POST 'https://api.speakai.co/v1/voice/testing/AGENT_ID/runs/RUN_ID/recommendations/REC_ID/apply' \
  -H 'x-speakai-key: sk_test_speak_0000000000000000' \
  -H 'x-access-token: eyJhbGciOiJIUzI1NiJ9.test-access-token.0000000000'
```

</CodePanel>

<ResponsePanel id="post-voice-testing-agent-id-runs-run-id-recommendations-rec-id-apply" statuses={[{"code":"200","description":"OK"},{"code":"400","description":"Invalid transition, or recommendation has no suggested fix"},{"code":"403","description":"Caller's role is not OWNER or ADMIN"},{"code":"404","description":"Agent, run, or recommendation not found"}]}>

<div slot="status-200">

**`200` OK**

Response body, `application/json`.

| Field | Type | Description |
| --- | --- | --- |
| `status` | string | One of: `success` . |
| `data` | object |  |
| `data.applied` | boolean |  |
| `data.recommendation` | object | The recommendation as stored on the run, now with applied: true and appliedAt set. |

</div>

<div slot="status-400">

**`400` Invalid transition, or recommendation has no suggested fix**

Response body, `application/json`.

| Field | Type | Description |
| --- | --- | --- |
| `status` | string | One of: `failed` . |
| `message` | string |  |

</div>

<div slot="status-403">

**`403` Caller's role is not OWNER or ADMIN**

Response body, `application/json`.

| Field | Type | Description |
| --- | --- | --- |
| `status` | string | One of: `failed` . |
| `message` | string |  |

Example response, `application/json`.

```json
{
  "status": "failed",
  "message": "You do not have permission to manage agents"
}
```

</div>

<div slot="status-404">

**`404` Agent, run, or recommendation not found**

Response body, `application/json`.

| Field | Type | Description |
| --- | --- | --- |
| `status` | string | One of: `failed` . |
| `requestId` | string |  |
| `code` | integer |  |
| `message` | string |  |
| `hints` | string[] |  |

Example response, `application/json`.

```json
{
  "status": "failed",
  "code": 404,
  "message": "Agent, run, or recommendation not found",
  "hints": []
}
```

</div>

</ResponsePanel>

</div>

## Suite

<h2 id="get-voice-testing-agent-id-suite">Get Test Suite</h2>

#### Get Test Suite

Returns the agent's test suite, or `null` in `data.suite` if none has been created yet, this is not a 404.

### Parameters

| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `agentId` | path | string | Yes |  |

<div slot="code">

<CodePanel label="Example request">

```bash
curl -X GET 'https://api.speakai.co/v1/voice/testing/AGENT_ID/suite' \
  -H 'x-speakai-key: sk_test_speak_0000000000000000' \
  -H 'x-access-token: eyJhbGciOiJIUzI1NiJ9.test-access-token.0000000000'
```

</CodePanel>

<ResponsePanel id="get-voice-testing-agent-id-suite" statuses={[{"code":"200","description":"OK"},{"code":"404","description":"Agent not found"}]}>

<div slot="status-200">

**`200` OK**

Response body, `application/json`.

| Field | Type | Description |
| --- | --- | --- |
| `status` | string | One of: `success` . |
| `data` | object |  |
| `data.suite` | object |  |

<details>
<summary>`data.suite` -- 11 more fields</summary>

| Field | Type | Description |
| --- | --- | --- |
| `_id` | string | MongoDB document id, included because every document has one; use suiteId to identify it. |
| `suiteId` | string |  |
| `agentId` | string |  |
| `companyId` | string |  |
| `scenarios` | object[] |  |
| `maxCostPerRun` | number |  |
| `autoRunOnKbUpdate` | boolean |  |
| `autoRunOnInstructionSave` | boolean |  |
| `scheduledCron` | string |  |
| `createdAt` | string (date-time) |  |
| `updatedAt` | string (date-time) |  |

<details>
<summary>`scenarios` -- 7 more fields</summary>

| Field | Type | Description |
| --- | --- | --- |
| `scenarioId` | string |  |
| `name` | string |  |
| `description` | string |  |
| `userMessages` | string[] | Required, at least one message. The scripted turns sent to the agent. |
| `criteria` | object[] | Defaults to an empty array. |
| `category` | string | One of: `greeting`, `kb_retrieval`, `off_topic`, `edge_case`, `custom` . |
| `isEnabled` | boolean |  |

<details>
<summary>`criteria` -- 10 more fields</summary>

| Field | Type | Description |
| --- | --- | --- |
| `criterionId` | string |  |
| `name` | string |  |
| `evaluationPrompt` | string | What the LLM judge is asked to evaluate. |
| `weight` | number |  |
| `isCritical` | boolean |  |
| `type` | string | Defaults to llm_judged when omitted. The other three route through a deterministic code check before the LLM judge runs. One of: `llm_judged`, `response_length`, `regex_match`, `tool_called` . |
| `maxWords` | integer | For type=response_length: fails if any agent response exceeds this word count. |
| `regexPattern` | string | For type=regex_match: JS regex source, no slashes. |
| `mustMatch` | boolean | For type=regex_match: when true (default), at least one response must match; when false, none may. |
| `expectedToolName` | string | For type=tool_called: the tool name to look for in the transcript's tool calls. |

</details>

</details>

</details>

</div>

<div slot="status-404">

**`404` Agent not found**

Response body, `application/json`.

| Field | Type | Description |
| --- | --- | --- |
| `status` | string | One of: `failed` . |
| `requestId` | string |  |
| `code` | integer |  |
| `message` | string |  |
| `hints` | string[] |  |

Example response, `application/json`.

```json
{
  "status": "failed",
  "code": 404,
  "message": "Agent not found",
  "hints": []
}
```

</div>

</ResponsePanel>

</div>

<h2 id="put-voice-testing-agent-id-suite">Create Or Update Test Suite</h2>

#### Update Test Suite

Requires the OWNER or ADMIN role. Upserts, creates the suite on first call, updates it on later calls. Send the full `scenarios` array you want to keep; it replaces the stored array rather than merging.

### Parameters

| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `agentId` | path | string | Yes |  |

### Request body

Fields marked **required** are the ones the server rejects the request without. Anything conditional, where a field becomes required only alongside another, is described under Request rules above rather than marked here.

| Field | Type |
| --- | --- |
| `scenarios` | object[] |
| `maxCostPerRun` | number |
| `autoRunOnKbUpdate` | boolean |
| `autoRunOnInstructionSave` | boolean |
| `scheduledCron` | string |

<details>
<summary>`scenarios` -- 7 more fields</summary>

| Field | Type | Description |
| --- | --- | --- |
| `scenarioId` | string |  |
| `name` | string |  |
| `description` | string |  |
| `userMessages` | string[] | Required, at least one message. The scripted turns sent to the agent. |
| `criteria` | object[] | Defaults to an empty array. |
| `category` | string | One of: `greeting`, `kb_retrieval`, `off_topic`, `edge_case`, `custom` . |
| `isEnabled` | boolean |  |

<details>
<summary>`criteria` -- 10 more fields</summary>

| Field | Type | Description |
| --- | --- | --- |
| `criterionId` | string |  |
| `name` | string |  |
| `evaluationPrompt` | string | What the LLM judge is asked to evaluate. |
| `weight` | number |  |
| `isCritical` | boolean |  |
| `type` | string | Defaults to llm_judged when omitted. The other three route through a deterministic code check before the LLM judge runs. One of: `llm_judged`, `response_length`, `regex_match`, `tool_called` . |
| `maxWords` | integer | For type=response_length: fails if any agent response exceeds this word count. |
| `regexPattern` | string | For type=regex_match: JS regex source, no slashes. |
| `mustMatch` | boolean | For type=regex_match: when true (default), at least one response must match; when false, none may. |
| `expectedToolName` | string | For type=tool_called: the tool name to look for in the transcript's tool calls. |

</details>

</details>

<div slot="code">

<CodePanel label="Example request">

```bash
curl -X PUT 'https://api.speakai.co/v1/voice/testing/AGENT_ID/suite' \
  -H 'x-speakai-key: sk_test_speak_0000000000000000' \
  -H 'x-access-token: eyJhbGciOiJIUzI1NiJ9.test-access-token.0000000000' \
  -H 'Content-Type: application/json' \
  -d '{}'
```

</CodePanel>

<ResponsePanel id="put-voice-testing-agent-id-suite" statuses={[{"code":"200","description":"OK"},{"code":"403","description":"Caller's role is not OWNER or ADMIN"},{"code":"404","description":"Agent not found"}]}>

<div slot="status-200">

**`200` OK**

Response body, `application/json`.

| Field | Type | Description |
| --- | --- | --- |
| `status` | string | One of: `success` . |
| `data` | object |  |
| `data.suite` | object |  |

<details>
<summary>`data.suite` -- 11 more fields</summary>

| Field | Type | Description |
| --- | --- | --- |
| `_id` | string | MongoDB document id, included because every document has one; use suiteId to identify it. |
| `suiteId` | string |  |
| `agentId` | string |  |
| `companyId` | string |  |
| `scenarios` | object[] |  |
| `maxCostPerRun` | number |  |
| `autoRunOnKbUpdate` | boolean |  |
| `autoRunOnInstructionSave` | boolean |  |
| `scheduledCron` | string |  |
| `createdAt` | string (date-time) |  |
| `updatedAt` | string (date-time) |  |

<details>
<summary>`scenarios` -- 7 more fields</summary>

| Field | Type | Description |
| --- | --- | --- |
| `scenarioId` | string |  |
| `name` | string |  |
| `description` | string |  |
| `userMessages` | string[] | Required, at least one message. The scripted turns sent to the agent. |
| `criteria` | object[] | Defaults to an empty array. |
| `category` | string | One of: `greeting`, `kb_retrieval`, `off_topic`, `edge_case`, `custom` . |
| `isEnabled` | boolean |  |

<details>
<summary>`criteria` -- 10 more fields</summary>

| Field | Type | Description |
| --- | --- | --- |
| `criterionId` | string |  |
| `name` | string |  |
| `evaluationPrompt` | string | What the LLM judge is asked to evaluate. |
| `weight` | number |  |
| `isCritical` | boolean |  |
| `type` | string | Defaults to llm_judged when omitted. The other three route through a deterministic code check before the LLM judge runs. One of: `llm_judged`, `response_length`, `regex_match`, `tool_called` . |
| `maxWords` | integer | For type=response_length: fails if any agent response exceeds this word count. |
| `regexPattern` | string | For type=regex_match: JS regex source, no slashes. |
| `mustMatch` | boolean | For type=regex_match: when true (default), at least one response must match; when false, none may. |
| `expectedToolName` | string | For type=tool_called: the tool name to look for in the transcript's tool calls. |

</details>

</details>

</details>

</div>

<div slot="status-403">

**`403` Caller's role is not OWNER or ADMIN**

Response body, `application/json`.

| Field | Type | Description |
| --- | --- | --- |
| `status` | string | One of: `failed` . |
| `message` | string |  |

Example response, `application/json`.

```json
{
  "status": "failed",
  "message": "You do not have permission to manage agents"
}
```

</div>

<div slot="status-404">

**`404` Agent not found**

Response body, `application/json`.

| Field | Type | Description |
| --- | --- | --- |
| `status` | string | One of: `failed` . |
| `requestId` | string |  |
| `code` | integer |  |
| `message` | string |  |
| `hints` | string[] |  |

Example response, `application/json`.

```json
{
  "status": "failed",
  "code": 404,
  "message": "Agent not found",
  "hints": []
}
```

</div>

</ResponsePanel>

</div>

<h2 id="post-voice-testing-agent-id-suite-generate">Generate Default Test Suite</h2>

#### Auto-Generate Test Suite

Requires the OWNER or ADMIN role. Uses the agent's own instructions and knowledge base to generate a default set of scenarios via an LLM call, then overwrites the suite's `scenarios` array with the result (upserting the suite if none exists). `maxCostPerRun` and the auto-run flags are left untouched.

### Parameters

| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `agentId` | path | string | Yes |  |

<div slot="code">

<CodePanel label="Example request">

```bash
curl -X POST 'https://api.speakai.co/v1/voice/testing/AGENT_ID/suite/generate' \
  -H 'x-speakai-key: sk_test_speak_0000000000000000' \
  -H 'x-access-token: eyJhbGciOiJIUzI1NiJ9.test-access-token.0000000000'
```

</CodePanel>

<ResponsePanel id="post-voice-testing-agent-id-suite-generate" statuses={[{"code":"200","description":"OK"},{"code":"403","description":"Caller's role is not OWNER or ADMIN"},{"code":"404","description":"Agent not found"},{"code":"503","description":"Suite generation is unavailable, the underlying LLM provider is not configured"}]}>

<div slot="status-200">

**`200` OK**

Response body, `application/json`.

| Field | Type | Description |
| --- | --- | --- |
| `status` | string | One of: `success` . |
| `data` | object |  |
| `data.suite` | object |  |

<details>
<summary>`data.suite` -- 11 more fields</summary>

| Field | Type | Description |
| --- | --- | --- |
| `_id` | string | MongoDB document id, included because every document has one; use suiteId to identify it. |
| `suiteId` | string |  |
| `agentId` | string |  |
| `companyId` | string |  |
| `scenarios` | object[] |  |
| `maxCostPerRun` | number |  |
| `autoRunOnKbUpdate` | boolean |  |
| `autoRunOnInstructionSave` | boolean |  |
| `scheduledCron` | string |  |
| `createdAt` | string (date-time) |  |
| `updatedAt` | string (date-time) |  |

<details>
<summary>`scenarios` -- 7 more fields</summary>

| Field | Type | Description |
| --- | --- | --- |
| `scenarioId` | string |  |
| `name` | string |  |
| `description` | string |  |
| `userMessages` | string[] | Required, at least one message. The scripted turns sent to the agent. |
| `criteria` | object[] | Defaults to an empty array. |
| `category` | string | One of: `greeting`, `kb_retrieval`, `off_topic`, `edge_case`, `custom` . |
| `isEnabled` | boolean |  |

<details>
<summary>`criteria` -- 10 more fields</summary>

| Field | Type | Description |
| --- | --- | --- |
| `criterionId` | string |  |
| `name` | string |  |
| `evaluationPrompt` | string | What the LLM judge is asked to evaluate. |
| `weight` | number |  |
| `isCritical` | boolean |  |
| `type` | string | Defaults to llm_judged when omitted. The other three route through a deterministic code check before the LLM judge runs. One of: `llm_judged`, `response_length`, `regex_match`, `tool_called` . |
| `maxWords` | integer | For type=response_length: fails if any agent response exceeds this word count. |
| `regexPattern` | string | For type=regex_match: JS regex source, no slashes. |
| `mustMatch` | boolean | For type=regex_match: when true (default), at least one response must match; when false, none may. |
| `expectedToolName` | string | For type=tool_called: the tool name to look for in the transcript's tool calls. |

</details>

</details>

</details>

</div>

<div slot="status-403">

**`403` Caller's role is not OWNER or ADMIN**

Response body, `application/json`.

| Field | Type | Description |
| --- | --- | --- |
| `status` | string | One of: `failed` . |
| `message` | string |  |

Example response, `application/json`.

```json
{
  "status": "failed",
  "message": "You do not have permission to manage agents"
}
```

</div>

<div slot="status-404">

**`404` Agent not found**

Response body, `application/json`.

| Field | Type | Description |
| --- | --- | --- |
| `status` | string | One of: `failed` . |
| `requestId` | string |  |
| `code` | integer |  |
| `message` | string |  |
| `hints` | string[] |  |

Example response, `application/json`.

```json
{
  "status": "failed",
  "code": 404,
  "message": "Agent not found",
  "hints": []
}
```

</div>

<div slot="status-503">

**`503` Suite generation is unavailable, the underlying LLM provider is not configured**

Response body, `application/json`.

| Field | Type | Description |
| --- | --- | --- |
| `status` | string | One of: `failed` . |
| `message` | string |  |

</div>

</ResponsePanel>

</div>

## Results

<h2 id="get-voice-testing-agent-id-baseline">Get Test Baseline</h2>

#### Get Baseline

The agent's best-scoring completed run, used to detect regressions on later runs. `null` in `data.baseline` if no run has completed yet.

### Parameters

| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `agentId` | path | string | Yes |  |

<div slot="code">

<CodePanel label="Example request">

```bash
curl -X GET 'https://api.speakai.co/v1/voice/testing/AGENT_ID/baseline' \
  -H 'x-speakai-key: sk_test_speak_0000000000000000' \
  -H 'x-access-token: eyJhbGciOiJIUzI1NiJ9.test-access-token.0000000000'
```

</CodePanel>

<ResponsePanel id="get-voice-testing-agent-id-baseline" statuses={[{"code":"200","description":"OK"},{"code":"404","description":"Agent not found"}]}>

<div slot="status-200">

**`200` OK**

Response body, `application/json`.

| Field | Type | Description |
| --- | --- | --- |
| `status` | string | One of: `success` . |
| `data` | object |  |
| `data.baseline` | object |  |

<details>
<summary>`data.baseline` -- 7 more fields</summary>

| Field | Type | Description |
| --- | --- | --- |
| `_id` | string | MongoDB document id, included because every document has one; use suiteId to identify it. |
| `agentId` | string |  |
| `suiteId` | string |  |
| `bestScore` | number |  |
| `bestRunId` | string |  |
| `scenarioBaselines` | object | Map of scenarioId to that scenario's best score. |
| `updatedAt` | string (date-time) |  |

</details>

</div>

<div slot="status-404">

**`404` Agent not found**

Response body, `application/json`.

| Field | Type | Description |
| --- | --- | --- |
| `status` | string | One of: `failed` . |
| `requestId` | string |  |
| `code` | integer |  |
| `message` | string |  |
| `hints` | string[] |  |

Example response, `application/json`.

```json
{
  "status": "failed",
  "code": 404,
  "message": "Agent not found",
  "hints": []
}
```

</div>

</ResponsePanel>

</div>

<h2 id="get-voice-testing-agent-id-score-history">Get Test Score History</h2>

#### Get Score History

Completed-run score points for charting, most recent first, capped at 100 regardless of `limit`. Only `status: completed` runs are included, a queued, running, paused, failed, or cancelled run never appears here.

### Parameters

| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `agentId` | path | string | Yes |  |
| `limit` | query | integer | No |  |

<div slot="code">

<CodePanel label="Example request">

```bash
curl -X GET 'https://api.speakai.co/v1/voice/testing/AGENT_ID/score-history' \
  -H 'x-speakai-key: sk_test_speak_0000000000000000' \
  -H 'x-access-token: eyJhbGciOiJIUzI1NiJ9.test-access-token.0000000000'
```

</CodePanel>

<ResponsePanel id="get-voice-testing-agent-id-score-history" statuses={[{"code":"200","description":"OK"},{"code":"404","description":"Agent not found"}]}>

<div slot="status-200">

**`200` OK**

Response body, `application/json`.

| Field | Type | Description |
| --- | --- | --- |
| `status` | string | One of: `success` . |
| `data` | object |  |
| `data.history` | object[] |  |

<details>
<summary>`data.history` -- 4 more fields</summary>

| Field | Type | Description |
| --- | --- | --- |
| `runId` | string |  |
| `score` | number |  |
| `timestamp` | string (date-time) |  |
| `trigger` | string | One of: `manual`, `kb_update`, `instruction_save`, `scheduled` . |

</details>

</div>

<div slot="status-404">

**`404` Agent not found**

Response body, `application/json`.

| Field | Type | Description |
| --- | --- | --- |
| `status` | string | One of: `failed` . |
| `requestId` | string |  |
| `code` | integer |  |
| `message` | string |  |
| `hints` | string[] |  |

Example response, `application/json`.

```json
{
  "status": "failed",
  "code": 404,
  "message": "Agent not found",
  "hints": []
}
```

</div>

</ResponsePanel>

</div>

## Related pages

- [API reference](/api/) for the base URL, authentication, and the error format.
- [Authenticate with the Speak AI API using access tokens](/api/authentication/)
- [Upload audio and video to Speak AI and read insights](/api/media/)
- [Create and update live transcription sessions](/api/live-transcription/)
- [Analyze text notes with the Speak AI text endpoints](/api/text/)

Get an API key on the [Speak AI developer page](https://app.speakai.co/developers?utm_source=docs&utm_medium=referral&utm_campaign=api-reference&utm_content=api-voice-agents/testing).

Source: https://docs.speakai.co/api/voice-agents/testing/index.mdx
