Skip to main content
An agent changes its behavior for two distinct reasons: because of what you say to it, and because of what its own system tells it about you. Authentication is the gate to that second axis — until the agent verifies who’s calling, it can’t look you up, so it can’t adapt to your account state, your tier, or your history. Many agents won’t do anything at all until they’ve authenticated the user: they ask for a phone number, a PIN, an account number, or a date of birth and verify it before continuing. To test that agent, your simulated user has to clear the same gate. This cookbook shows how to authenticate the simulated user — starting from a single fixed user, then varying the user (and therefore the backend state the agent sees) across test cases. There are two mechanisms for authenticating the simulated user:
  • In the conversation — the persona supplies the credentials when the agent asks for them, entering them on the keypad (DTMF) or saying them aloud. Works for every agent type.
  • Through the connection — you pass the user’s identity to the agent out of band, in a request header or payload, so your backend resolves the account before the first turn. Agent-type-specific; see Passing the user through the connection.
Whichever approach you use, the user you authenticate as must actually exist in your system, set up in the state your test needs. You’re impersonating a real record your agent can look up — not inventing one.

The simplest case: one QA user

If every test runs as the same user, you don’t need attributes or per-test-case setup. Put the credentials in a persona and use that persona for every simulation. This requires a QA user that already exists in your database with the right setup — a real record the agent can authenticate against. Create that account first (a real phone number and PIN the agent will accept), in whatever state your test needs. In the persona’s Persona Characteristics prompt, describe the credentials and exactly when to enter them. For a voice agent, the persona enters digits as DTMF tones with the dtmf tool rather than speaking them — spoken digits are prone to speech-to-text errors, and most IVRs expect keypad input anyway. Give it explicit, structured instructions:
<authentication>
- Wait for the agent to finish its prompt before entering anything.
- When the agent asks for your phone number, use the dtmf tool to
  enter: 6501234567
- When the agent asks for your PIN, use the dtmf tool to enter: 1234
- Enter digits only via the dtmf tool. Never say the numbers aloud.
- If the agent says the input was invalid, re-enter the same value
  once, then continue.
</authentication>
Then launch all your simulations with this persona. Every run authenticates as the same user before the real test begins.

Varying the user per test case

To exercise that second axis — what the agent’s system tells it about you — authenticate as a different user in each test case, so the agent looks up different backend state each time. The first axis you drive through the conversation; this one you drive through which authenticated user you present as. For example, suppose your agent behaves differently depending on whether the caller already has an appointment. You’d want:
  • one test case that authenticates as a user who has already booked an appointment (the agent checks and sees it), and
  • one that authenticates as a user who has not — the same thing said, two different agent behaviors.
You don’t need a separate persona for each. Keep one persona and substitute the credentials with test-case attributes, so each test case supplies its own user:
<authentication>
- Wait for the agent to finish its prompt before entering anything.
- When the agent asks for your phone number, use the dtmf tool to
  enter: {{test_case.phone_number}}
- When the agent asks for your PIN, use the dtmf tool to enter:
  {{test_case.pin}}
- Enter digits only via the dtmf tool. Never say the numbers aloud.
</authentication>
Then each test case carries its own values. Test case — existing appointment:
{
  "phone_number": "6501234567",
  "pin": "1234"
}
Test case — no appointment:
{
  "phone_number": "6509998888",
  "pin": "5678"
}
Coval substitutes each test case’s values into the persona prompt before the call, so the same persona authenticates as a different real user on every run — and you observe how the agent’s behavior changes based on what its system knows about that user. Both of these users, again, must exist in your system in the state you’re testing for.

Passing the user through the connection

Sometimes you don’t want the user to authenticate in the conversation at all — you want the agent to already know who’s calling when the conversation starts. Most agent types let you pass the user’s identity (a user_id, account ID, or user token) directly through the connection, so your backend resolves the right account before the first turn. This is different from your agent authenticating Coval (API keys, bearer tokens on your webhook) — that’s covered in each connection type’s own page. The table below is only about passing the user.
Connection typeHow you pass the userVaries per test case?
Chat (HTTP)Put the user’s identifier in custom_headers (e.g. X-Account-Id) or in the request body via the input_templateYes — both accept {{test_case.*}} and {{agent.*}}, resolved per test case
Inbound VoiceOn a SIP connection, carry user_id / account in custom_sip_headers. A plain PSTN phone number can’t pass headers — authenticate in-conversation instead.Yes (SIP) — SIP headers accept {{test_case.*}}, set via mutations
Outbound VoicePut the user’s identity (e.g. account_id, user_token) in the trigger_call_payload; phone_number_key selects which number is dialedYes — the trigger payload accepts {{test_case.*}}
Chat A2ACarry a user identifier in custom_headers (A2A message bodies are text-only, so identity rides in headers)No — headers are static per agent
Chat WebSocketSend the user in initialization_json on connect, or in custom_headers on the handshakeNo — resolved per agent, not per test case
WebSocket (voice)Send the user in initialization_json after the upgrade, or in custom_headers on the upgradeNo — resolved per agent, not per test case
SMSThe phone number identifies the userNumber is per agent; any other identity in-conversation
Pipecat · OpenAI Realtime · Gemini LiveNo transport hook to pass the userAuthenticate in-conversation (persona supplies credentials)
Two things to take away from the table:
  • Per-test-case identity works on Chat, Inbound SIP, and Outbound. Those three substitute {{test_case.*}} into the header, SIP header, or payload, so one test set can drive many different users out of band — the same way the persona prompt does in the conversation. On the other connection types the connection config is fixed per agent, so to vary the user per test case you authenticate in-conversation.
  • For voice-to-voice and plain PSTN, in-conversation is the only option. OpenAI Realtime, Gemini Live, and Pipecat connect Coval straight to the model or platform, and a PSTN phone number strips custom headers — there’s no transport to carry the user’s identity, so the persona/DTMF approach from the sections above is how you authenticate.