Embed an interview
Run a live AI interview inside your own product. The interview renders in an iframe you
control, your page drives it with postMessage, and you receive events as the
conversation happens — without handling WebRTC, microphone permissions, or audio
pipelines yourself.
How the pieces fit
- Your serverRabbitt
Create the interview
Create one interview for the candidate from your trusted backend.
POST /v1/interviews - Your serverRabbitt
Mint a short-lived embed token
Rabbitt returns the scoped token to your server, never your API key to the browser.
POST /v1/interviews/:id/embed-token - Your serverYour page
Render the interview frame
Send the embed URL or token to your page and mount the iframe.
<iframe src="…?t=EMBED_TOKEN"> - Your pageRabbitt
Run and control the conversation
The frame exchanges its token server-side; your page sends commands and receives postMessage events.
postMessage - Your serverRabbitt
Read the analysis
Fetch the report from your server after the conversation has ended.
GET /v1/interviews/:id/report
Two credentials are involved, and the distinction matters:
- Your API key never leaves your server.
- The embed token is short-lived, scoped to one interview, bound to your origins, and is the only thing that reaches the browser.
1. Register your origins
On the API key you will use, add every origin that will frame the
interview — for example https://app.yourcompany.com. Include your local development
origin (http://localhost:3000) while building.
Minting an embed token with no registered origins returns 403 NO_ALLOWED_ORIGINS. That
is deliberate: an empty allowlist denies everything rather than acting as a wildcard.
2. Create the interview
const interview = await fetch("https://api.rabbitt.ai/api/v1/interviews", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.RABBITT_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
agentId: process.env.RABBITT_INTERVIEW_AGENT_ID,
candidateName: candidate.fullName,
candidateEmail: candidate.email,
externalRef: candidate.id,
}),
}).then((r) => r.json());Do this once per candidate, when the interview is scheduled.
3. Mint an embed token
Do this immediately before rendering the page, not at schedule time — the token is deliberately short-lived.
const embed = await fetch(
`https://api.rabbitt.ai/api/v1/interviews/${interview.id}/embed-token`,
{
method: "POST",
headers: {
Authorization: `Bearer ${process.env.RABBITT_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ expiresInSeconds: 900 }),
},
).then((r) => r.json());
// { token, embedUrl, expiresAt, allowedOrigins }4. Render the iframe
The simplest path is the loader script, which creates the frame and wraps the message protocol:
<div id="interview" style="height: 640px"></div>
<script src="https://voice.rabbitt.ai/embed.js"></script>
<script>
const session = RabbittInterview.mount(document.getElementById("interview"), {
token: "EMBED_TOKEN_FROM_YOUR_SERVER",
onEvent(event) {
if (event.type === "session.ended") {
window.location.href = "/interviews/complete";
}
},
});
// Drive it from your own UI.
document.getElementById("end").onclick = () => session.end();
</script>Or place the iframe yourself and speak the protocol directly:
<iframe
src="https://voice.rabbitt.ai/embed/interview/INTERVIEW_ID?t=EMBED_TOKEN"
allow="microphone"
style="width: 100%; height: 640px; border: 0"
></iframe>5. The postMessage protocol
Every message is { source: "rabbitt-embed", version: 1, type, payload }.
Messages you send
| Type | Effect |
|---|---|
start | Begin the interview |
end | End it and trigger report generation |
mute / unmute | Toggle the candidate's microphone |
setTheme | { theme: "light" | "dark" } to match your app |
Events you receive
| Type | Payload | Meaning |
|---|---|---|
ready | { sessionId, candidateName, scenario } | The frame loaded and exchanged its token |
session.started | { sessionId } | The conversation began |
state.changed | { state } | connecting, listening, ai_speaking, ai_thinking, ended |
transcript.turn | { speaker, content } | A turn completed — for live captions |
session.ended | { sessionId, reportId? } | Finished |
error | { code, message } | Something failed |
resize | { height } | Content height changed, for auto-sizing the frame |
Handling it yourself
const frame = document.querySelector("iframe");
window.addEventListener("message", (event) => {
// Always verify the origin. Without this check any page in any other frame
// could send you forged events.
if (event.origin !== "https://voice.rabbitt.ai") return;
const message = event.data;
if (message?.source !== "rabbitt-embed") return;
switch (message.type) {
case "ready":
send("start");
break;
case "state.changed":
setStatus(message.payload.state);
break;
case "session.ended":
onComplete(message.payload.reportId);
break;
}
});
function send(type, payload = {}) {
frame.contentWindow.postMessage(
{ source: "rabbitt-embed", version: 1, type, payload },
"https://voice.rabbitt.ai",
);
}6. Read the report
session.ended may arrive before the analysis is ready. Fetch the report from your
server, retrying on 202:
const report = await fetch(
`https://api.rabbitt.ai/api/v1/interviews/${interviewId}/report?includeTranscript=true`,
{ headers: { Authorization: `Bearer ${process.env.RABBITT_API_KEY}` } },
);
// 202 => still generating, try again shortlyTroubleshooting
The frame renders blank or refuses to load. The parent origin is not on the API key's
allowed origins. Origins must match exactly, including scheme and port —
https://app.example.com does not cover https://staging.app.example.com.
401 INVALID_EMBED_TOKEN. The token expired. They are minutes-long by design; mint
one when the page is served rather than reusing one.
The microphone never activates. Check allow="microphone" on the iframe, and that
your own page is served over HTTPS — browsers block microphone access on insecure origins.
409 INTERVIEW_ALREADY_COMPLETED. Each interview is a single session. Create a new
one to let a candidate retake it.