API ReferenceOverview

API Reference

Every SDK call — @ear3/voice-interviewer, @ear3/server — is a thin wrapper over these HTTP endpoints. Reach for the REST API when a client library isn’t available or you need to script something bespoke.

Base URL: https://app.ear3.ai

Authentication

Two key types, two auth headers.

Publishable keys (pk_…)

Safe to expose in browsers. Passed as a body field on session creation, not as a header.

POST /api/v2/sdk/sessions
Content-Type: application/json
 
{
  "publishableKey": "pk_test_…",
  "interviewId": "dpl_…",
  "metadata": { "userId": "42" }
}

Secret keys (sk_…)

Server-only. Passed as a bearer token.

GET /api/v2/sdk/sessions/inv_abc123…
Authorization: Bearer sk_test_…

Webhook signing (whsec_…)

Not sent on requests — used to verify incoming webhooks on your server. See Webhooks below.


Sessions

A session is one respondent’s attempt at a deployed interview. Each mount of <Ear3Interview> / <Ear3VoiceInterview> in @ear3/voice-interviewer creates a fresh session via the endpoint below.

POST /api/v2/sdk/sessions

Create a session for a deployment.

Body

FieldTypeRequiredNotes
publishableKeystringpk_test_… or pk_live_…
interviewIdstringDeployment id from the dashboard
metadataobjectRound-trips into webhook + session.retrieve

Response 201 Created

{
  "sessionId": "inv_abc123…",
  "sessionUrl": "https://app.ear3.ai/v2/responder/abc123?from=sdk",
  "expiresAt": "2026-07-01T18:00:00Z"
}

Iframe the sessionUrl — that’s where the voice pipeline lives. The sessionId is what you’ll receive back in the webhook and pass to GET /sessions/:id.

GET /api/v2/sdk/sessions/{sessionId}

Retrieve session state. Requires sk_ bearer. Publishable keys are rejected here (no CORS either) so this call must be made from your server.

Response 200 OK

{
  "id": "inv_abc123…",
  "interviewId": "dpl_…",
  "status": "COMPLETED",
  "createdAt": "2026-07-01T17:12:03Z",
  "clickedAt": "2026-07-01T17:12:40Z",
  "completedAt": "2026-07-01T17:15:11Z",
  "metadata": { "userId": "42" },
  "url": "https://app.ear3.ai/v2/responder/abc123?from=sdk",
  "expiresAt": "2026-07-01T18:00:00Z",
  "response": {
    "id": "rsp_…",
    "status": "COMPLETED",
    "transcriptKey": "s3://ear3-transcripts/…",
    "summary": "User wants dark mode by default and shorter onboarding."
  }
}

Statuses: PENDINGSENTCLICKEDSTARTEDCOMPLETED (or EXPIRED).


Webhooks

Ear3 delivers webhooks when interview lifecycle events happen. Every delivery is HMAC-signed with your whsec_… key.

Signature header

Ear3-Signature: t=1719849823,v1=5f8a3c1b09e4…
  • t — Unix timestamp (seconds) of the signing time
  • v1= — HMAC-SHA256 of ${t}.${rawBody} using whsec_… as the key

You may see multiple v1= values in the same header during a key rotation window; consider a match on any of them a success.

Verifying (Node)

import crypto from 'node:crypto'
 
function verify(rawBody: string, header: string, secret: string) {
  const parts = Object.fromEntries(
    header.split(',').map((p) => p.split('=')),
  )
  const ts = Number(parts.t)
  if (Math.abs(Date.now() / 1000 - ts) > 300) return false // 5-min window
 
  const expected = crypto
    .createHmac('sha256', secret)
    .update(`${parts.t}.${rawBody}`)
    .digest('hex')
 
  return crypto.timingSafeEqual(
    Buffer.from(expected),
    Buffer.from(parts.v1),
  )
}

🛑 Verify against the raw body, not JSON.parse(...)d. Any whitespace change breaks the signature.

If you’re on Node, @ear3/server’s ear3.webhooks.constructEvent() does all of this — see the SDK reference.

Event payload shape

{
  "id": "evt_2xY…",
  "type": "interview.completed",
  "created": 1719849823,
  "data": {
    "sessionId": "inv_abc123…",
    "interviewId": "dpl_…",
    "metadata": { "userId": "42" },
    "completedAt": "2026-07-01T17:15:11Z"
  }
}

Event types

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

Subscribe per-endpoint in the dashboard (Settings → Webhooks). New event types may be added — handle unknown types gracefully in your handler.

Delivery guarantees

  • Timeout: 8 seconds per attempt
  • Success: any 2xx response
  • Retries: not currently — a 4xx or 5xx means the event is dropped (retries with exponential backoff are on the roadmap)

Every attempt is logged to webhook_deliveries in your workspace — inspect it if something didn’t fire the way you expected.


Errors

Every non-2xx response follows a common shape.

{
  "error": "interview_not_found",
  "message": "Deployment dpl_xyz does not exist in your workspace"
}
Statuserror codeWhen
400invalid_requestMissing / malformed field in the body
401invalid_keyMissing or unrecognized pk_ / sk_
403cross_workspaceKey + resource belong to different workspaces
404interview_not_foundinterviewId doesn’t resolve
404session_not_foundsessionId doesn’t resolve
409interview_not_activeDeployment status is DRAFT or PAUSED
410interview_expiredDeployment expiresAt is in the past
429rate_limit_exceededHourly or daily cap hit for this workspace
500internal_errorSomething on our side — retry with exponential backoff

The @ear3/server SDK maps every non-2xx to an Ear3Error with .status and .code fields matching the values above.


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