SDKTroubleshooting

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_KEY in .env.local — it must start with pk_test_ or pk_live_ and match a key shown on Settings → API Keys
  • Restart npm run dev after 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="" without allow-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:

  1. Is the endpoint URL public? localhost won’t work. Use ngrok or a Vercel preview (see Deployment)
  2. Is the endpoint enabled? Check webhook_endpoints.disabledAt is null
  3. Is the event type subscribed? The endpoint’s enabledEvents must include interview_completed (or whichever you're waiting on)
  4. Did the org have a whsec_ key when the event fired? Without one, emission logs a warning and skips. Create a key + retry.
  5. 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) is https://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


Built by Ear3 — voice interviews for any app.
⌘/