---
title: "Simulations"
description: "Run one-shot or batched AI caller simulations against an agent or a phone number, follow their progress, and inspect graded transcripts when they finish."
---

Simulations let you programmatically validate an agent: an AI test
caller role-plays a scenario against your agent, and the run captures
the resulting call, transcript, and grade. Each run produces a real
[call log](/api-reference/calls). Billing itemizes the selected agent,
the simulated caller, and any telephony leg; `charge_multiplier` is retained
as a response field for compatibility and is `1` for current runs. Use
**runs** for one-shot tests and **batches**
to execute several scenarios (or repetitions) with staggered starts.

For scripted, repeatable regression testing — saved scenarios, AI
scenario generation, condition matrices, and release-gate suites — see
[Test scenarios & suites](/api-reference/test-scenarios).

## Endpoints

| Method | Path | Description |
|--------|------|-------------|
| `GET` | `/v1/simulations` | List simulation runs |
| `POST` | `/v1/simulations` | Start a single simulation run |
| `GET` | `/v1/simulations/batches` | List batches |
| `POST` | `/v1/simulations/batches` | Start a batch |
| `GET` | `/v1/simulations/batches/{batch_id}` | Retrieve batch status and child run ids |

<Note>
  **Deprecated aliases.** `/v1/test-calls` (list + create runs),
  `/v1/test-call-batches`, and `/v1/test-call-batches/{batch_id}`
  are older aliases for the same handlers. They keep working, but new
  integrations should use the canonical `/v1/simulations` routes.
</Note>

<a id="test-call-run-object"></a>

## Simulation run object

```json
{
  "id": 7110,
  "target_type": "agent",
  "target_id": 12,
  "target_name": "Customer Support Agent",
  "agent_name": "Customer Support Agent",
  "direction": "outbound",
  "mode": "bot",
  "target_number": "",
  "scenario_prompt": "Roleplay as a customer asking about refunds.",
  "status": "completed",
  "charge_multiplier": 1,
  "estimated_cost_cents": 16,
  "call_id": 987654321,
  "error_message": "",
  "metadata": {},
  "requested_by_id": 7,
  "started_at": "2026-04-20T18:24:10.113Z",
  "completed_at": "2026-04-20T18:25:04.822Z",
  "created_at": "2026-04-20T18:24:09.011Z",
  "updated_at": "2026-04-20T18:25:04.822Z",
  "duration_seconds": 54,
  "recording_url": "https://storage.googleapis.com/…",
  "transcript": [
    { "role": "user", "content": "Hi, I want a refund.", "timestamp": null }
  ],
  "grade": {
    "id": 88,
    "status": "completed",
    "score": 92,
    "call_outcome": "success",
    "outcome_reasoning": "…",
    "summary": "…",
    "rubric_breakdown": {},
    "detected_issues": [],
    "grader_model": "heuristic-v1",
    "grader_version": "v1",
    "graded_at": "2026-04-20T18:25:11.002Z"
  }
}
```

| Field | Type | Description |
|-------|------|-------------|
| `id` | integer | Simulation run id |
| `target_type` | string | `agent` or `phone_number` — what the test caller exercises |
| `target_id` | integer \| null | Id of the target agent / phone number |
| `target_name` | string | Name of the target (agent name or phone number) |
| `agent_name` | string \| null | The voice agent's name, resolved even for `phone_number` targets |
| `direction` | string | `outbound` (test caller places the call) or `inbound` (test caller answers) |
| `mode` | string | How the run was wired internally: `bot` (bot-to-bot) or `sip` (SIP loopback). Read-only — not settable on create |
| `target_number` | string | Optional E.164 override for the remote side; empty uses the platform default |
| `scenario_prompt` | string | Free-form role-play prompt driving the test caller |
| `status` | string | `queued`, `in_progress`, `completed`, `failed` |
| `charge_multiplier` | number | Compatibility field. Current runs use `1`; pricing is itemized per bot and telephony leg |
| `estimated_cost_cents` | integer \| null | Pre-call estimate for both sides, in USD cents |
| `call_id` | integer \| null | The [call log](/api-reference/calls) the run produced |
| `error_message` | string | Populated on `status="failed"` |
| `metadata` | object | Internal diagnostics (e.g. start errors) |
| `requested_by_id` | integer \| null | User who started the run (null for API-key runs) |
| `started_at`, `completed_at` | timestamp \| null | Run lifecycle |
| `duration_seconds` | integer \| null | Derived from the underlying call |
| `recording_url` | string \| null | Short-lived playable recording URL |
| `transcript` | array | Simplified turns: `{role, content, timestamp}` |
| `grade` | object \| null | Latest [grade](/api-reference/calls#ai-call-grading) of the underlying call, or `null` if ungraded |

---

## List runs

<CodeGroup>
```bash cURL
curl 'https://api.thunderphone.com/v1/simulations?limit=100' \
  -H "Authorization: Bearer sk_live_YOUR_API_KEY"
```
</CodeGroup>

### Query parameters

| Param | Type | Description |
|-------|------|-------------|
| `limit` | integer | Default 100, max 500 |
| `offset` | integer | |
| `target_type` | string | Filter by `agent` or `phone_number` |
| `target_id` | integer | Filter by target id (combine with `target_type`) |
| `status` | string | Filter by status |

Returns a plain array of [simulation run objects](#simulation-run-object),
newest first. Listing also self-heals runs whose completion signal was
lost (stale `queued`/`in_progress` runs are marked `failed`).

---

## Start a single run

<CodeGroup>
```bash cURL
curl -X POST https://api.thunderphone.com/v1/simulations \
  -H "Authorization: Bearer sk_live_YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "target_type": "agent",
    "target_id":   12,
    "direction":   "outbound",
    "scenario_prompt": "Roleplay as a customer asking about refunds.",
    "consent_to_charge": true
  }'
```

```python Python
run = requests.post(
    "https://api.thunderphone.com/v1/simulations",
    headers={"Authorization": "Bearer sk_live_YOUR_API_KEY"},
    json={
        "target_type": "agent",
        "target_id": 12,
        "direction": "outbound",
        "scenario_prompt": "Roleplay as a customer asking about refunds.",
        "consent_to_charge": True,
    },
).json()
```
</CodeGroup>

### Request fields

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `target_type` | string | yes | `agent` or `phone_number` |
| `target_id` | integer | yes | Id of the target in this org |
| `direction` | string | no | `outbound` (default) or `inbound` |
| `target_number` | string | no | E.164 for the remote side — defaults to the platform test number |
| `scenario_prompt` | string | no | ≤ 4000 chars. Drives the test caller |
| `language` / `primary_language` | string | no | Language for the test caller (ISO 639 or BCP-47; normalized server-side; unsupported codes are rejected) |
| `simulator_product` | string | no | Turn engine for the simulated caller: `testing` (default, bot-vs-bot guards) or `spark` (behaves like a responsive human — useful for warm-transfer consult testing) |
| `consent_to_charge` | boolean | yes | Must be `true` — required to prevent accidental charges |

Returns `201 Created` with the [run object](#simulation-run-object).
Status starts at `queued` and advances to `completed` or `failed`;
poll the list endpoint or
[`GET /v1/calls/{call_id}`](/api-reference/calls#retrieve-a-call) once
`call_id` is set.

Errors:

| Status | Condition |
|--------|-----------|
| `400` | Validation failure (e.g. `consent_to_charge` missing / false) |
| `402` | Insufficient balance |
| `404` | Target agent or phone number not found |
| `429` | More than 10 simulation runs already `queued`/`in_progress` in the org |
| `429` | `concurrent_call_limit_reached`: the organization is at its [concurrent-call limit](/api-reference/introduction#call-limits); retry after `Retry-After` seconds |
| `502` | The run could not be started (run is marked `failed` with an `error_message`) |

---

## Batches

A batch executes N staggered runs against the same target. The
response returns immediately with a batch in `in_progress`; poll the
detail endpoint for progress and per-run results.

### Batch object

```json
{
  "id": 201,
  "name": "",
  "target_type": "agent",
  "target_id": 12,
  "direction": "outbound",
  "target_number": "",
  "scenario_prompts": ["Ask about refunds.", "…"],
  "matrix_dimensions": null,
  "run_count": 5,
  "stagger_seconds": 2,
  "status": "in_progress",
  "total_runs": 5,
  "completed_runs": 0,
  "failed_runs": 0,
  "estimated_minutes": 10,
  "estimated_cost_cents": 80,
  "error_message": "",
  "requested_by_id": 7,
  "started_at": "2026-04-20T18:24:09.011Z",
  "completed_at": null,
  "created_at": "2026-04-20T18:24:09.011Z",
  "updated_at": "2026-04-20T18:24:09.011Z",
  "run_ids": [7110, 7111, 7112, 7113, 7114]
}
```

| Field | Type | Description |
|-------|------|-------------|
| `id` | integer | Batch id |
| `name` | string | Optional label |
| `scenario_prompts` | array of string | Prompts rotated across runs |
| `matrix_dimensions` | object \| null | Set when the batch was produced by a [scenario matrix execution](/api-reference/test-scenarios) — records which condition axes were expanded |
| `run_count`, `stagger_seconds` | integer | As requested |
| `status` | string | Batch status |
| `total_runs`, `completed_runs`, `failed_runs` | integer | Progress counters |
| `estimated_minutes` | integer \| null | Pre-flight duration estimate |
| `estimated_cost_cents` | integer \| null | Pre-flight cost estimate |
| `run_ids` | array of integer | Child [run](#simulation-run-object) ids — fetch runs via `GET /v1/simulations` |

### List batches

`GET /v1/simulations/batches` — query params `status`, `limit`
(default 100, max 500), `offset`. Returns a plain array of batch
objects, newest first.

### Start a batch

<CodeGroup>
```bash cURL
curl -X POST https://api.thunderphone.com/v1/simulations/batches \
  -H "Authorization: Bearer sk_live_YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "target_type":  "agent",
    "target_id":    12,
    "direction":    "outbound",
    "run_count":    5,
    "stagger_seconds": 2,
    "scenario_prompts": [
      "Ask about refunds.",
      "Ask for hours of operation.",
      "Complain about a delayed shipment.",
      "Ask to speak with a human.",
      "Ask an unrelated trivia question."
    ],
    "consent_to_charge": true
  }'
```
</CodeGroup>

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `target_type` | string | yes | `agent` or `phone_number` |
| `target_id` | integer | yes | |
| `direction` | string | no | `outbound` (default) or `inbound` |
| `run_count` | integer | no | 1–20, default 3 |
| `stagger_seconds` | integer | no | Delay between spawning successive runs (0–60, default 5) |
| `scenario_prompts` | array of string | no | Up to 20 prompts (≤ 4000 chars each), rotated per run (modulo) |
| `target_number` | string | no | |
| `name` | string | no | ≤ 255 chars |
| `language` / `primary_language` | string | no | Language for the test callers |
| `consent_to_charge` | boolean | yes | Must be `true` |

Returns `201 Created` with the [batch object](#batch-object). Errors
match [single-run errors](#start-a-single-run) (`400`/`402`/`404`).

### Get batch detail

<CodeGroup>
```bash cURL
curl https://api.thunderphone.com/v1/simulations/batches/201 \
  -H "Authorization: Bearer sk_live_YOUR_API_KEY"
```
</CodeGroup>

Returns the [batch object](#batch-object), or `404`. `run_ids` is a
list of child-run ids, not nested run objects — fetch each run via
[`GET /v1/simulations`](#list-runs) or follow the `call_id` on each
run to [`GET /v1/calls/{call_id}`](/api-reference/calls#retrieve-a-call).

---

## Related

<CardGroup cols={2}>
  <Card title="Test scenarios & suites" icon="list-check" href="/api-reference/test-scenarios">
    Saved scenarios, AI scenario generation, condition matrices, and release-gate suites.
  </Card>
  <Card title="Agents" icon="robot" href="/api-reference/agents">
    Build the agent being tested — simulations exercise the agent's draft config.
  </Card>
  <Card title="Calls" icon="phone" href="/api-reference/calls">
    Inspect the resulting call logs and transcripts.
  </Card>
  <Card title="Billing" icon="credit-card" href="/api-reference/billing">
    Simulation charges itemize the selected agent, simulated caller, and any telephony leg.
  </Card>
</CardGroup>
