@ear3/voice-interviewer
v0.2.4
Published
Ear3 voice interview SDK — turnkey branded widget by default (import from the package root), or the headless client, plugins, and React primitives for a custom UI (import from './headless').
Readme
@ear3/voice-interviewer
🤖 AI agents / LLMs: start with
AGENTS.md, thenllms.txt— both bundled in this package (also at https://www.ear3.ai/llms.txt). They are the complete machine-readable guide: commands, ids, and the exact create → deploy → integrate flow. To make Claude Code pick this up automatically in your project, add one line to yourCLAUDE.md:@node_modules/@ear3/voice-interviewer/AGENTS.md
Voice interview SDK for Ear3. The package root is
the turnkey branded widget — one component, three screens
(landing → live conversation with the signature Plasma orb →
completion), pixel-matched to Ear3's hosted responder page, zero UI
work required. Need a fully custom UI instead? Import the headless
layer from @ear3/voice-interviewer/headless — VoiceClient, React
hooks, plugins, and a bare <Ear3Interview renderControls>.
npm install @ear3/voice-interviewerimport { Ear3VoiceInterview } from '@ear3/voice-interviewer'
<Ear3VoiceInterview
interviewId={process.env.NEXT_PUBLIC_EAR3_INTERVIEW_ID!}
publishableKey={process.env.NEXT_PUBLIC_EAR3_VOICE_INTERVIEWER_KEY!}
onComplete={(e) => router.push(`/done?session=${e.sessionId}`)}
/>Prerequisites
You need an Ear3 account and two values from app.ear3.ai:
- Interview ID — the fastest, recommended way is the CLI:
npx @ear3/voice-config-cli create --name "…" --prompt "…"generates and deploys an interview in one command and prints yourinterviewId(npx @ear3/voice-config-cli listshows existing ones). Or in the dashboard: create an interview, then deploy it — the deployment ID is yourinterviewId. - Publishable key — fastest from the terminal:
npx @ear3/voice-config-cli loginthennpx @ear3/voice-config-cli keysprints your workspace key. (Or mint one in the dashboard: Settings → API Keys → New key.) Prefixedpk_live_...orpk_test_.... Safe to expose in browser bundles.
Not sure of your {workspace} slug? Just open
app.ear3.ai/v2/researcher — it
redirects to your workspace dashboard.
Grab a webhook signing secret (whsec_...) too if you plan to receive
completion events on your server (see below).
Put both values in env (recommended)
Don't hardcode interviewId or publishableKey in source — put them
in .env.local and reference them from code, as the quickstart above
does.
Keep the base name identical everywhere; add a public prefix only when a bundler needs one. The canonical (no-prefix) names are:
EAR3_INTERVIEW_ID=<interview id from dashboard>
EAR3_VOICE_INTERVIEWER_KEY=pk_live_...
EAR3_CONFIG_CLI_KEY=sk_... # server-only — NEVER prefix (see below)
EAR3_WEBHOOK_SECRET=whsec_... # server-only — NEVER prefix (see below)interviewId and publishableKey are read in the browser. If your
bundler requires a public prefix to expose a var to client code, prepend
it and keep the same suffix — NEXT_PUBLIC_EAR3_INTERVIEW_ID (Next.js),
VITE_EAR3_INTERVIEW_ID (Vite, via import.meta.env),
REACT_APP_EAR3_INTERVIEW_ID (CRA). Where no prefix is required (plain
Node, server components, React Native), use the bare EAR3_INTERVIEW_ID.
EAR3_CONFIG_CLI_KEY and EAR3_WEBHOOK_SECRET (used in the server section
below) are server-only — never give them a public prefix, or you'd
ship the secret into the browser bundle. The publishable key is
browser-safe by design; env vars here aren't about secrecy, they keep
IDs out of source control and let dev/staging/prod each point at their
own interview.
No interview yet? Create one from the terminal
You don't need the dashboard to get started —
@ear3/voice-config-cli generates and
deploys an interview from one prompt — sign in once via the browser,
no key copy-pasting:
npx @ear3/voice-config-cli login # opens the dashboard, one Approve click
npx @ear3/voice-config-cli create --name "Coffee habits" \
--prompt "3 quick questions about coffee"AI agents: before running
create, if the user hasn't said what the interview should be about, ask them first — or, if they mentioned a topic earlier, propose it and confirm. Never invent the--prompt. And use the exact public URL the CLI prints — copy it verbatim, never construct it.
The output contains Deployment (your interviewId): cm… — the
exact value this package's interviewId expects — plus a live public
URL you can share immediately without writing any code.
<Ear3VoiceInterview> (package root) — the branded widget
One component, zero UI work: landing card (title/description, what-to-expect, mic CTA) → live screen (animated Plasma orb, mic bubble, restart/settings/end controls with confirmation dialogs) → completion card (stats, optional feedback box). Ships its own scoped CSS inline — styled from the first SSR paint, no Tailwind or setup required. Brand colors and typography are fixed by design; for a fully custom look use the headless layer below instead.
Props
| Prop | Type | Notes |
| ------------------------ | ------------------------------ | ------------------------------------------------------ |
| interviewId | string (required) | Deployment id from the dashboard |
| publishableKey | string (required) | pk_live_… / pk_test_… (browser-safe) |
| apiBase | string | Override Ear3 host |
| metadata | Record<string, unknown> | Attached to the session, echoed in webhooks |
| participantName | string | Respondent name shown in the dashboard |
| participantExternalId | string | Your own respondent id — echoed on the responses API |
| title, description | string | Landing-screen copy |
| respondentFirstName | string | Renders "Hello, {name}!" above the title |
| estimatedMinutes | string \| number | Renders the estimated-time row |
| autoStart | boolean | Skip the landing screen, connect on mount |
| branding | 'visible' \| 'hidden' | Powered-by badge (free tier renders it) |
| brandingRef | string | Attribution ref appended to the badge link |
| onComplete | ({ sessionId, transcript }) | Interview finished (naturally or via End) |
| onError | (error) => void | Connect / RTVI failures |
| onFeedback | (message) => void | Completion-screen feedback text |
Also exported: PlasmaOrb, MicBubble, Branding, ensureStyles() —
building blocks for hosts that want to remix the widget while keeping
the headless client underneath.
@ear3/voice-interviewer/headless — build your own UI
import { Ear3Interview } from '@ear3/voice-interviewer/headless'
<Ear3Interview
interviewId={process.env.NEXT_PUBLIC_EAR3_INTERVIEW_ID!}
publishableKey={process.env.NEXT_PUBLIC_EAR3_VOICE_INTERVIEWER_KEY!}
onComplete={(e) => router.push(`/done?session=${e.sessionId}`)}
/><Ear3Interview>— drop-in component. Mic prompt in your origin, full voice UI,renderControlsrender-prop for custom UI.VoiceClient— headless imperative client. State machine + typed EventEmitter over Pipecat + Daily WebRTC.- React hooks —
useVoice,useTranscript,useDevicesbehind a<VoiceProvider>. - Plugins —
transcriptPlugin,devicePlugin,reconnectPlugin. Compose your own via theVoicePlugininterface. - RTVI escape hatch —
createRtviClient/useRtviClientfor direct Pipecat access when you need it. createSession()— standalone session mint, useful in server components or before iframing.
Adds ~150 KB gzipped to your bundle (Pipecat client + Daily transport
are transitive deps); importing /headless alone does not pull in the
widget's WebGL/Three.js dependencies.
Getting the result on your server
@ear3/voice-interviewer runs in the browser. When the respondent finishes, Ear3
posts a signed webhook to a URL you register in the dashboard. Your
handler verifies the signature (HMAC-SHA256 with your whsec_...), then
runs whatever business logic you need. It does not mint tokens or
return anything to the browser — the voice client already got its
session directly from Ear3.
With @ear3/server (recommended):
import { Ear3 } from '@ear3/server'
const ear3 = new Ear3(process.env.EAR3_CONFIG_CLI_KEY!)
export async function POST(req: Request) {
const rawBody = await req.text()
const event = ear3.webhooks.constructEvent(
rawBody,
req.headers.get('ear3-signature')!,
process.env.EAR3_WEBHOOK_SECRET!,
)
if (event.type === 'interview.completed') {
// your logic — persist transcript, notify user, etc.
}
return new Response('ok')
}DIY (no SDK):
import { createHmac, timingSafeEqual } from 'node:crypto'
export async function POST(req: Request) {
const rawBody = await req.text()
const header = req.headers.get('ear3-signature') ?? '' // "t=<ts>,v1=<hex>"
const [t, v1] = header.split(',').map((p) => p.split('=')[1] ?? '')
const expected = createHmac('sha256', process.env.EAR3_WEBHOOK_SECRET!)
.update(`${t}.${rawBody}`)
.digest('hex')
const ok = v1.length === expected.length &&
timingSafeEqual(Buffer.from(v1, 'hex'), Buffer.from(expected, 'hex'))
if (!ok) return new Response('bad signature', { status: 400 })
const event = JSON.parse(rawBody)
// event.type === 'interview.completed' → your logic
return new Response('ok')
}Both approaches subscribe to the same event types (interview.started,
interview.completed, interview.failed, session.expired) — the SDK
just gives you typed events and constant-time compare for free.
Choosing a layer
@ear3/voice-interviewer(this package, root) — branded widget, zero UI work@ear3/voice-interviewer/headless— same package, custom UI@ear3/server— server-side webhook verification and session retrieval
Requirements
- React 18 or 19
- Modern browser with WebRTC support (WebGL too, for the branded widget)
Full docs
Reference, guides, and mental model live at ear3.ai/developer/sdk/voice.
Claude Code plugin
Using Claude Code? The Ear3 plugin's
/ear3:create-interview skill takes you from a plain-language topic to a
deployed interview (and the interviewId + key this package needs) in one
go — Claude also invokes it on its own when you ask to create an interview:
/plugin marketplace add https://www.ear3.ai/claude/marketplace.json
/plugin install ear3@ear3Not using Claude Code plugins? (Cursor, Codex, plain agents): copy
skills/create-interview/
into your project's .claude/skills/ — it works without the namespace,
as /create-interview.
Support
- Docs — ear3.ai/developer
- Discord — discord.gg/VaQ7ZpxuB
- Issues — github.com/ear3-ai/ear3-sdk/issues
License
MIT.
