Replaying Conversations

Replay re-runs something that already happened, so you can test a change without placing a call. There are two levels:

EndpointWhat it re-runsReturns
TurnPOST /conversation_items/{id}/replayOne assistant turn, with the conversation up to that point as contextThe response text (and tool calls) the assistant would have produced, synchronously
CallPOST /conversations/{id}/replayThe caller’s recorded audio, through an agent, end to endA new conversation you can inspect, evaluate, and extract from

Turn replay is the fast loop: it takes seconds, doesn’t consume audio, and can generate up to 200 samples of the same turn in one request. This page covers it first.

Replay a single turn

A conversation item is one turn. Only assistant items from ended conversations can be replayed.

Getting a conversation item ID

The easy way — copy it from the dashboard. No code needed:

  1. Open the conversation in the Phonic dashboard.
  2. Find the assistant turn you want to re-run.
  3. Click the menu at the right of that turn.
  4. Choose Copy item ID.

You now have a conv_item_... ID on your clipboard — paste it straight into the request below.

If you’d rather find turns in code — say you’re replaying every turn of every call in a project — walk the conversation’s items instead. Assistant items are the replayable ones, and each item’s id is its conversation item ID:

1import { PhonicClient } from "phonic";
2import type { Phonic } from "phonic";
3
4const client = new PhonicClient({ apiKey: process.env.PHONIC_API_KEY });
5
6// 1. Find a conversation (or use an ID you already have)
7const list = await client.conversations.list({ project: "my-project" });
8const conversationId = list.conversations[0].id;
9
10// 2. Fetch it — the conversation carries its turns in `items[]`
11const { conversation } = await client.conversations.get(conversationId);
12
13// 3. Each assistant item is a replayable turn; `item.id` is the conversation item ID
14const assistantTurns = conversation.items.filter(
15 (item): item is Phonic.ConversationItem => item.role === "assistant",
16);
17const conversationItemId = assistantTurns[7].id; // e.g. the 8th assistant turn

Replaying the turn

With the ID in hand, replay it. Omit system_prompt to re-run the turn exactly as it originally ran:

1const { responses } = await client.conversationItems.replay(conversationItemId, {
2 num_responses: 100,
3});

Both body fields are optional:

FieldDefaultNotes
system_promptThe prompt the turn originally ran withSet it to the candidate prompt you want to test. Everything else about the turn — the conversation history, the agent’s tools, the model — stays as it was.
num_responses1Up to 200. Responses are sampled independently, so they will differ from each other.

The response is a flat list, one entry per sample:

1{
2 "responses": [
3 {
4 "text": "Sure — I can help with that. What time works best?",
5 "tool_calls": []
6 }
7 ]
8}

Tool calls in tool_calls are what the assistant would have invoked. Replay never actually calls your tools, so nothing in your systems is booked, charged, or transferred.

Sampling: run the same turn 100 times

A single replay tells you what the assistant can say; a hundred replays tell you what it usually says. Because generation is sampled, one lucky response is not evidence that a prompt change worked — for a flaky behavior, the number you care about is the fraction of samples that get it right.

num_responses: 100 takes roughly 15–20 seconds and typically comes back with close to 100 distinct texts. Score them yourself:

1const { responses } = await client.conversationItems.replay(conversationItemId, {
2 system_prompt: candidatePrompt,
3 num_responses: 100,
4});
5
6const ended = responses.filter((response) =>
7 response.tool_calls.some(
8 (call) => call.tool.name === "natural_conversation_ending",
9 ),
10).length;
11
12console.log(`ended the call ${ended}/${responses.length} of the time`);

Run the same measurement with system_prompt omitted to get the baseline for the prompt that actually ran, then compare. A useful workflow:

  1. Find a turn that went wrong — often one flagged by a failing eval prompt.
  2. Replay it 100× as-is. This is your baseline rate.
  3. Replay it 100× with the candidate system_prompt. Keep the change if the rate moves.
  4. Update the agent and re-run your evals on new calls: a turn replay holds one turn’s context fixed, so it cannot tell you what the change does to the rest of the call.

To iterate by hand rather than by script, use the prompt playground in the dashboard: in the conversations view, open the menu next to a turn. It calls the same endpoint.

Replay a whole call

Turn replay holds the conversation history fixed. When a prompt change should alter the course of the call — the agent asks for the account number earlier, so every later turn differs — replay the conversation instead. This re-runs the caller’s recorded audio through an agent as a real conversation:

1await client.conversations.replay(conversationId, { agent: "support-agent" });

agent is optional and defaults to the agent the conversation originally ran with; point it at a different agent to test a full prompt or config change. The call returns as soon as the replay starts, and the replay shows up as a new conversation with origin: "replay" in the project it belongs to — list conversations to find it, then evaluate or extract against it like any other call.

Conversation replay does not need an agent-server or telephony leg — it works for conversations that ran over LiveKit or the WebSocket API as well as inbound and outbound phone calls.

When replay is unavailable

StatusMeaning
409The conversation is still live. Wait for it to end.
422 (turn)The item isn’t an assistant turn, or the conversation’s transcripts were deleted.
422 (call)The conversation has no audio, its recordings were deleted, or it has no associated agent.
429 (call)Your concurrency limit is reached. Retry, or run replays with less overlap.
503 (call)Not enough capacity to start the replay right now. Retry.