Skip to main content
In this cookbook you build a voice interview that runs in the candidate’s browser. The same pattern fits any timed, spoken session: screening interviews, mock interviews, oral assessments, language practice or structured feedback calls.

What you’ll build

  • A candidate opens your page, allows the microphone and talks to an AI interviewer that greets them by name.
  • The page shows live captions for both speakers and whether the interviewer is listening, thinking or speaking.
  • The interview keeps time on its own. At 8:00 the interviewer stops starting new topics, at 9:30 it thanks the candidate and says goodbye, and 10:00 is a hard stop. If the candidate gives no answer for 60 seconds, the interviewer closes politely.
  • While the interview runs, a moderator on your side can nudge the interviewer or end the interview with a spoken goodbye.
  • When it ends, your backend receives one webhook with the full transcript and a structured score, tagged with your own interview and candidate IDs.
  • No audio is stored, so the transcript is your record of the interview.
You need: a workspace API key (sk_live_…) and webhook secret (whsec_…), a backend in Python 3.10+ or Node.js 18+, and an HTTPS URL that can receive webhooks (a tunnel to your laptop works while you build).
Keep the API key on your server. The browser only receives two per-call links, and they work only for that one interview.

How it works


Step 1: Create the interviewer

An agent holds everything that is the same for every interview of one kind: the interviewer’s instructions, voice and language, how it ends a call, and how the finished conversation is scored. Everything about one candidate goes on the call in step 2.

Write the instructions

The agent’s system_prompt holds up to 8,000 characters. An interview guide fits comfortably if it contains only what the interviewer needs during the conversation. Move anything that is only needed afterwards, such as the scoring rubric, into analysis, which has its own instructions. This layout works well for voice interviews:
system_prompt
The {{placeholders}} are filled per interview from the call’s variables (up to 32 variables, 512 characters each). One agent then serves every candidate for that role type. See the prompting guide for more on writing for voice.
Keep one agent per interview type (for example “Sales screening” and “Support screening”) rather than one agent per candidate. Changing a prompt is then a single edit, and every interview of that type stays comparable.

If your interview guide is longer than 8,000 characters

The 8,000-character limit on system_prompt is the same for every workspace, and there is no per-call system prompt. A call can add its variables, its first_message and, while it runs, instructions. Long interview guides usually carry material the interviewer does not need while it talks. Move each part to where it is used: A shorter prompt also makes replies faster, because the model reads the prompt on every turn.

Score the interview automatically

Turn on analysis and the agent scores each finished interview against your rubric. The result arrives with the transcript in step 5.

Create the agent

interviewer.json
Keep a person in the loop. The score is a summary that helps your team review interviews faster. A person should make the hiring decision, so the schema above offers advance or human_review and deliberately has no reject option. Automated decisions about people are regulated in many places, including under the GDPR and India’s DPDP Act.

Step 2: Start an interview from your backend

When a candidate opens the interview page, your backend creates the call with everything specific to this candidate, stores the call ID beside its own interview record, and gives the page two links. Send an Idempotency-Key built from your interview ID. If the request is retried, you get the same call back instead of a second one.
server.py
What to know about the two links:
  • ws_url is the audio socket. It works only once and must be opened within 5 minutes. If the candidate refreshes the page, create a new interview.
  • events_url is the live caption stream. You can reopen it at any point during the interview and for one hour after it ends.
  • If the page never connects, the call ends as failed with ended_reason: "no-media" and is not billed.

Step 3: Build the interview page

The page asks for the microphone, starts the interview, plays the interviewer’s voice, and shows captions, the interviewer’s state and a countdown. The audio format is described under Audio socket on the Browser calls page.
interview.html
Choices you can change:
  • Muting while the interviewer speaks. With the box ticked, the page sends silence while agent.state is speaking, so room noise is never taken for an answer and the candidate cannot interrupt. speaking follows the actual audio, not the text. Untick it to let candidates talk over the interviewer, which feels more natural in a quiet room. Keep sending frames either way: silence keeps the line open, and the silence timer counts only real speech.
  • Captions. Each caption appears when its turn is complete. An interrupted interviewer turn shows only the words the candidate actually heard.
  • Leaving. Closing the audio socket ends the interview immediately. To end with a spoken goodbye instead, call your backend’s end route from step 4.

Step 4: Let a moderator steer the interview

While the interview runs, a moderator, such as a recruiter watching the captions, can change the interviewer’s direction or end the interview with a goodbye. Your backend does both through live control.
  • An instruction is added to the interviewer’s context from its next reply and stays for the rest of the interview. It is never read aloud and does not appear in the transcript.
  • A close speaks a goodbye line and ends the interview as completed, with ended_reason: "api-ended-call".
Useful instructions for interviews:
server.py (continued)
A moderator view can show the same captions as the candidate. Give your backend the call’s events_url, or read the stream server-side with your API key. See live events.

End at once

close lets the interviewer say goodbye first. When the interview must stop immediately, for example because the candidate reports a problem, abort it instead. The audio stops at once and the page receives call.ended. The call ends aborted and is not billed.

Optional: give the interviewer time checks

The timing policy already handles the wrap-up at 8:00 and the goodbye at 9:30. If you also want the interviewer to pace itself earlier, send it the remaining time as instructions. Start the clock on the call.started webhook: it fires when audio goes live, which is the moment the timing policy counts from.
server.py (continued)
Instructions add up. Each one stays in the interviewer’s context for the rest of the interview, so keep them few and short.

Step 5: Receive the transcript and the score

Your webhook receives these events for each interview, in order:
  1. call.started when the candidate’s audio connects. Use it to start any time checks from step 4.
  2. A terminal event as soon as the interview ends: call.completed, call.failed or call.aborted. Mark the interview as over.
  3. call.processed, after the transcript has settled (within about two minutes) and the score is ready. It carries the transcript in data.transcript and the score in data.call.analysis.
Every event carries your metadata, so you can find the interview without a lookup table. Verify the signature on every delivery, and ignore any event whose id you have already seen. The Webhooks page explains signature verification.
server.py (continued)
What the parts of call.processed mean for an interview: If your webhook was down, deliveries are retried. You can also read GET /v2/calls/{id} (it reports transcript_status) and GET /v2/calls/{id}/transcript at any time.

Test it

Short timers show every behaviour in about two minutes. Use them while you build, then switch back to the 10-minute values.
Test values for POST /v2/calls
Run three interviews:
  1. Talk for the full two minutes. At 1:00 the interviewer stops starting new topics. At 1:40 it says the close message and the page shows “Time’s up”. Your webhook receives call.completed, then call.processed with ended_reason: "time-limit-close", the transcript and a score.
  2. Say one sentence, then stay silent. About 30 seconds later the interviewer says the silence message and the interview ends as user-silence-close.
  3. Steer and end from the moderator routes. An instruction returns applied and the next reply follows it. The end route makes the interviewer say goodbye; the interview ends as api-ended-call.

Privacy and compliance

  • Tell candidates up front that they are talking to an AI interviewer, that the conversation is transcribed, and how the transcript and score will be used. Get their consent before the interview starts.
  • With recording_enabled: false, no recording is stored and GET /v2/calls/{id}/recording returns 404. The transcript is kept. See data retention for how long. To turn recording off for your whole workspace, ask help@miraiminds.co.
  • Put only IDs in metadata. Never put names, contact details or interview answers there.
  • Keep a person in the loop for hiring decisions. Use the score to sort and review interviews, and do not use it to reject candidates automatically.
  • ws_url and events_url give access to one interview. Send them only to that candidate’s page over HTTPS, and keep them out of logs and analytics. Your API key stays on your server.
  • Treat captions as untrusted text and render them with textContent, never as HTML.

Production checklist

  • One agent per interview type, with a Timekeeping section and an analysis rubric.
  • metadata carries your interview and candidate IDs; your database stores the returned call id.
  • Every create uses an Idempotency-Key built from your interview ID.
  • max_duration_secs, timing, recording_enabled: false, final_results: true and webhook_url are set on every interview.
  • Moderator routes check that the caller is your staff and use idempotency keys.
  • The webhook verifies signatures, ignores repeated event IDs, answers quickly and processes call.processed in the background.
  • Your interview status is driven by the terminal event, and your transcript processing by call.processed.
  • Candidates see an AI disclosure and give consent before the interview starts.
  • You ran the three test interviews above on the tier you will use in production.

Common questions

Yes, from your backend: POST /v2/calls/{id}/control with {"type": "instruction", "text": "…"}. The agent follows it from its next reply. If the action starts in the browser, the page asks your backend, which holds the key. See step 4.
Yes. timing on the call gives the agent a wrap-up instruction a set number of seconds before the cap, then a spoken goodbye before the cap, and a polite close after a stretch of user silence. The clock starts when audio goes live. To make the agent pace itself earlier, send time checks.
Yes. The create response returns the call id. Use a close control for a spoken goodbye, or POST /v2/calls/{id}/abort to stop at once. See step 4.
Yes. Open events_url with EventSource: every completed turn arrives as a transcript.turn event for both speakers, in real time. Text arrives per completed turn, not word by word. Audio travels over a WebSocket (ws_url) and events over Server-Sent Events, so you need no other client library. See step 3.
Yes. agent.state events report speaking, listening, processing and ended. speaking follows the actual audio, so it is safe for muting the microphone while the agent talks, as the page in step 3 does.
No. It is a platform limit, the same for every workspace, and there is no per-call system prompt. See If your interview guide is longer than 8,000 characters for where each part of a long guide belongs.
Yes. first_message on the call replaces the agent’s opening line for that call only. Your backend can write it in full, with no placeholders. If you do use a {{placeholder}} and its value is missing, it is replaced with nothing, so the agent never reads out the braces.
Yes, unchanged, in data.call.metadata of every webhook for the call and on GET /v2/calls/{id}. The create response also returns the call id to store.
The terminal event (call.completed, call.failed or call.aborted) means the conversation is over. call.processed, sent when you create the call with final_results: true, means “everything is ready”: it carries the transcript and the score. Process the session on call.processed. See step 5.
Yes. webhook_url on the call applies to that call only.
It arrives inline in call.processed (data.transcript.turns), and you can fetch it any time with GET /v2/calls/{id}/transcript. It is normally ready by the time the terminal webhook arrives; transcript_status on the call says ready. call.processed follows within about two minutes of the call ending, plus the time scoring takes.
Yes. Create the call with recording_enabled: false: no audio is stored, and captions, the transcript and scoring still work. To turn audio off for your whole workspace, ask help@miraiminds.co. See data retention for how long transcripts are kept.
Everything in this cookbook uses POST /v2/calls with channel: "web". Build new browser sessions on it.

Browser calls

Audio format, live events, live control and timing reference.

Webhooks

Event types, signature verification, retries and final results.

Agents

Every agent field, voices and ending a call.

Limits

Prompt, metadata, timing and data-retention limits.