Troubleshooting
Most common failure modes and how to fix them. Ordered by likelihood.
For the full Ear3VoiceError code reference, see Errors.
”Failed to load interview: API key not found”
You're passing a publishableKey that Ear3 doesn't recognize.
- Double-check
NEXT_PUBLIC_EAR3_VOICE_INTERVIEWER_KEYin.env.local— it must start withpk_test_orpk_live_and match a key shown on Settings → API Keys - Restart
npm run devafter editing.env.local(Next.js doesn't hot-reload env vars in client components)
“Failed to load interview: Interview not found”
Your interviewId doesn't resolve to a deployment, OR the deployment
belongs to a different workspace than your key.
- Confirm the id you copied from the dashboard. It looks like
cm…(cuid), not a UUID. - Confirm the API key and the interview are in the same organization. SDK calls reject cross-org access with a 403.
”Failed to load interview: Interview is not active” (409)
The deployment exists but its status is DRAFT or PAUSED. Go to
the dashboard, open the deployment, and flip it to Active.
”Failed to load interview: Interview has expired” (410)
The deployment has an expiresAt in the past. Either bump it in the
dashboard or remove the expiry.
”Invalid signature” on every webhook
You're JSON-parsing the body before verifying. The signature is computed over the exact raw bytes — any whitespace change breaks it.
// ❌ wrong
const body = await req.json()
ear3.webhooks.constructEvent(JSON.stringify(body), …)
// ✅ right
const rawBody = await req.text()
ear3.webhooks.constructEvent(rawBody, …)Express equivalent: use express.raw({ type: 'application/json' }),
not express.json().
”Timestamp outside the tolerance zone” on every webhook
Your server clock is more than 5 minutes off from Ear3’s. Run ntpdate
or check your container’s clock source. Default tolerance is 300
seconds; you can widen it but fix the clock instead — it’ll bite you
elsewhere.
<Ear3Interview> mounts twice on every render
You’re recreating the metadata object every render. The component
includes interviewId and publishableKey in its useEffect deps but
deliberately excludes metadata to avoid this — if you’re still seeing
re-mounts, suspect React StrictMode (double-mount in dev only) or a
parent component remounting.
To verify:
<Ear3Interview
key="onboarding-stable" // pin a stable React key
…
/>“ear3:complete never fires”
The responder posts ear3:complete to window.parent when the
invitation status flips to COMPLETED. Things to check:
- Are you embedding via iframe? The component does — if you mounted it
inside an
iframe sandbox=""withoutallow-same-origin, postMessage is blocked. - Network tab: there should be a
GET /api/v2/invitations/<hash>every 3s (polling) while the interview is in progress. - Is the responder URL using
?from=sdk? It is by default — if you manually constructed it, you need the param.
Webhook never arrives
Run through these in order:
- Is the endpoint URL public?
localhostwon’t work. Use ngrok or a Vercel preview (see Deployment) - Is the endpoint enabled? Check
webhook_endpoints.disabledAtis null - Is the event type subscribed? The endpoint’s
enabledEventsmust includeinterview_completed(or whichever you're waiting on) - Did the org have a
whsec_key when the event fired? Without one, emission logs a warning and skips. Create a key + retry. - Look at the audit row:
SELECT status, response_status, error_message, attempted_at
FROM webhook_deliveries
WHERE endpoint_id = '<your-endpoint-id>'
ORDER BY attempted_at DESC
LIMIT 5;FAILED rows tell you exactly what went wrong (timeout, 4xx, 5xx,
connection refused).
“secretKey must start with sk_”
You passed a pk_… or whsec_… value to new Ear3(). That class is
server-only and accepts secret keys only. For browser embeds, use
<Ear3Interview publishableKey={…}> from @ear3/voice-interviewer.
CORS error in browser console on /api/v2/sdk/sessions
The endpoint already sets Access-Control-Allow-Origin: * for the
session-create call. If you’re seeing CORS errors anyway:
- You’re calling a non-Ear3 host. Confirm
apiBase(or the SDK default) ishttps://app.ear3.ai. - You’re calling
GET /api/v2/sdk/sessions/<id>from the browser — that endpoint is secret-key only and intentionally doesn't send CORS headers. Move that call to your server.
Diagnosing a VoiceClient session
@ear3/voice-interviewer stays silent in the browser console by default. To trace
server messages, disconnect() calls, and transcript-persist attempts,
pass debug: true:
const client = new VoiceClient({
interviewId,
publishableKey,
debug: true, // logs are gated behind this — off by default
})Don’t ship debug: true to production — it logs full transcript
snapshots and session identifiers to the consumer’s console. Warnings
about a missing invitationHash or a failed transcript persist
(console.warn) print regardless of debug, since those indicate a
broken integration rather than routine tracing.
Anything not covered
- Search the @ear3/voice-interviewer reference or @ear3/server reference for the function giving you trouble
- Check
webhook_deliveries.error_messagefor the exact failure text - File an issue at github.com/ear3-ai/ear3-templates/issues