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

Request and event flow
Your serverHolds your API key
Your pageHosts the iframe
RabbittRuns the interview
  1. Your server
    Rabbitt

    Create the interview

    Create one interview for the candidate from your trusted backend.

    POST /v1/interviews
  2. Your server
    Rabbitt

    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
  3. Your server
    Your page

    Render the interview frame

    Send the embed URL or token to your page and mount the iframe.

    <iframe src="…?t=EMBED_TOKEN">
  4. Your page
    Rabbitt

    Run and control the conversation

    The frame exchanges its token server-side; your page sends commands and receives postMessage events.

    postMessage
  5. Your server
    Rabbitt

    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

javascript
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.

javascript
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:

html
<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:

html
<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

TypeEffect
startBegin the interview
endEnd it and trigger report generation
mute / unmuteToggle the candidate's microphone
setTheme{ theme: "light" | "dark" } to match your app

Events you receive

TypePayloadMeaning
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

javascript
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:

javascript
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 shortly

Troubleshooting

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.

Dashboard Welcome