SDKConcepts

Concepts

The mental model for the SDK. Read this once and the API stops being mysterious.

Object hierarchy

Organization              ← your workspace at ear3.ai
  └── Survey              ← the interview "design" (questions + flow)
        └── Deployment    ← a published, takeable version of the survey
              └── Session ← one respondent's attempt at the deployment
                    └── Response ← persisted answers + transcript

You create the first three in the dashboard (one-time setup). interviewId in the SDK = the deployment id.

The SDK creates the bottom two automatically every time someone hits your <Ear3Interview>. A new session + response per mount — no clones, no copies of the underlying survey or deployment.

API key types

Three flavors, all generated from Settings → API Keys.

TypePrefixWhere it livesWhat it can do
Publishablepk_live_…Browser bundles, OK to exposeCreate sessions for your org’s deployments
Secretsk_live_…Server env onlyEverything — list, retrieve, admin operations
Webhookwhsec_…Server env onlyVerify incoming webhook signatures (HMAC-SHA256)

Each type has _live_ and _test_ modes. Test mode keys can be generated freely; use them in CI and local development.

Critical: the webhook signing secret is not an authentication credential — it’s a shared symmetric key. Both Ear3 and your server hold the same value. Anyone with it can forge events, so treat it like any other secret.

How a session is created

Two endpoints, doing different things. Both verify the publishable key the same way (SHA-256 hash lookup) and both confirm the deployment belongs to the same org.

Ear3VoiceInterview / Ear3Interview (native path) → POST /api/v2/sdk/connect

  1. Ear3 mints a SurveyInvitation and spins up a Pipecat Cloud worker for the room, returning { room_url, token }
  2. The VoiceClient inside the SDK connects to that Daily room directly via @pipecat-ai/daily-transport
  3. Voice + RTVI events flow through your React tree — no iframe, no postMessage

Standalone session URL (createSession / server SDK) → POST /api/v2/sdk/sessions

  1. Ear3 creates a SurveyInvitation (anonymous, no email) and an InvitationLink with a unique hash
  2. Returns { sessionId, sessionUrl, expiresAt } where sessionUrl is https://app.ear3.ai/v2/responder/<hash>?from=sdk
  3. Point a respondent at that URL (email link, server component) — the hosted responder page runs the whole voice / RTVI flow

Same underlying session model in both cases. Each mount = one fresh session; reload the parent page = another session. The underlying deployment is never modified.

Call createSession when you need a session URL without loading the voice component — no React involved.

How the SDK connects

@ear3/voice-interviewer runs the RTVI client directly in your React tree. The same Pipecat Cloud worker sits on the other end — you talk to it via @pipecat-ai/client-js in your bundle. No iframe, no postMessage.

Your app (parent page)
├── @ear3/voice-interviewer
│    └── <Ear3VoiceInterview> / <Ear3Interview>
│         ├── POST /api/v2/sdk/connect  ─────► app.ear3.ai
│         │                                   └─► spawns Pipecat Cloud worker
│         │                                   └─► returns { room_url, token }
│         │
│         ├── @pipecat-ai/client-js         ← RTVI client (in YOUR bundle)
│         │    └── @pipecat-ai/daily-transport
│         │         │
│         │         │   audio in/out ◄──► Daily SFU ◄──► Pipecat Cloud worker
│         │         │                                     (STT · LLM · TTS · VAD)
│         │         │
│         │         └── RTVI events → your UI (transcripts, bot-speaking)
│         │
│         └── Default branded UI or your custom renderControls
└─ onComplete(event) fires when bot emits `interview.completed` server-message

Branded widget vs. headless

Same package, two levels of control. Both call the same backend, mint identical sessions, and fire the same webhooks — they differ only in how much of the UI you own.

Aspect<Ear3VoiceInterview> (branded)<Ear3Interview> / VoiceClient (headless)
Importpackage root/headless subpath
Voice UI controlEar3’s branded turnkey UI, zero workFull — renderControls or fully custom
Bundle size added~150 KB (Pipecat client + Daily transport)~150 KB
Mic permission promptAsked in your originAsked in your origin
RTVI events (transcripts)Handled for youFull RTVI stream via callbacks
Sessions / webhooksIdenticalIdentical

Neither is “advanced” — pick whichever fits your product. Want a working voice interview in the afternoon? Use the branded widget. Building a custom voice UI (own waveform, own transcript pane, own turn-taking indicator)? Reach for the headless client.

See @ear3/voice-interviewer for the full guide.

The metadata channel

This is how you correlate a session with whatever you call a “user” in your app. Pass any JSON-serializable object:

<Ear3Interview
  interviewId={…}
  publishableKey={…}
  metadata={{
    userId: currentUser.id,
    plan: 'pro',
    cohort: 'march-2026',
  }}
/>

It round-trips into:

  • event.data.metadata in the webhook payload your server receives
  • session.metadata when you call ear3.sessions.retrieve(sessionId)

Use it. It’s the only stable identifier that connects “this voice interview” to “this user record in my database”.

Webhook flow

Respondent finishes interview

Ear3 marks SurveyInvitation.status = COMPLETED

Ear3 finds all WebhookEndpoints for the org that subscribe to
  `interview_completed` and have status `enabled`

For each endpoint:
  Build payload { id, type: "interview.completed", created, data: {…} }
  Sign:  HMAC-SHA256( whsec_ key , `${unix_ts}.${rawBody}` )
  POST:  Headers include  Ear3-Signature: t=<ts>,v1=<sig>
  Log:   webhook_deliveries row (status, response code, body, error)

Your /api/webhooks/ear3 handler receives it

@ear3/server  ear3.webhooks.constructEvent(rawBody, signature, whsec)
  - parses + verifies signature with constant-time compare
  - rejects timestamps older than 5 min (default)
  - returns typed Ear3Event

Switch on event.type → your business logic

Delivery is fire-and-forget on Ear3’s side with an 8-second timeout per attempt. A 2xx counts as success. (Retries are roadmapped — see Troubleshooting.)

Event types

EventWhen it fires
interview.startedRespondent opened the link and the session began
interview.completedRespondent finished all questions
interview.failedVoice pipeline crashed mid-interview
session.expiredLink expired before the respondent reached the end

You subscribe per-endpoint when you register the webhook in the dashboard. New event types may be added — handle unknown types gracefully (the template already does — see app/api/webhooks/ear3/route.ts).

What lives where

ConcernWhere it actually runs
Question/flow designEar3 dashboard (you don’t write the questions in your code)
Voice pipeline (STT, LLM, TTS)Ear3’s Pipecat infrastructure
Session creation APIEar3 backend (called by the browser SDKs)
Transcript storageEar3 backend
Webhook signingEar3 backend
Your responsibilityMount the component, handle the webhook

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