@ear3/server
Server SDK. Retrieve sessions, mint invite links, and verify webhook
signatures. Requires Node 18+ (native fetch + node:crypto).
npm install @ear3/servernew Ear3(secretKey, options?)
import { Ear3 } from '@ear3/server'
const ear3 = new Ear3(process.env.EAR3_CONFIG_CLI_KEY!) // sk_live_… or sk_test_…Options
| Option | Type | Notes |
|---|---|---|
baseUrl | string | Override Ear3 host. Falls back to EAR3_BASE_URL env, then https://app.ear3.ai |
fetchOptions | RequestInit | Default fetch options applied to every request |
The constructor throws if secretKey doesn’t start with sk_ —
publishable keys belong in the browser SDK
(@ear3/voice-interviewer), not here.
ear3.sessions.create(params)
const session = await ear3.sessions.create({
interviewId: 'dpl_…',
participantName: 'Olena K.', // dashboard display name (optional)
participantExternalId: 'crm_42', // your own respondent id (optional)
metadata: { userId: '42' },
})Returns { sessionId, sessionUrl, expiresAt }. Useful for emailing
session links, batch invitations, or pre-warming a session in a
server component before iframing it.
ear3.sessions.retrieve(sessionId)
const session = await ear3.sessions.retrieve('inv_…')Returns:
{
id: string // same as the sessionId you passed
interviewId: string
status: 'PENDING' | 'SENT' | 'CLICKED' | 'STARTED' | 'COMPLETED' | 'EXPIRED'
createdAt: string // ISO
clickedAt: string | null
completedAt: string | null
metadata: Record<string, unknown> | null // what you passed in
url: string | null // /v2/responder/<hash> absolute URL
expiresAt: string | null
response: {
id: string
status: string
transcriptKey: string | null
summary: string | null
} | null
}Use this in your webhook handler if you need more than what
event.data carries (e.g. the full transcript pointer).
Webhook event types
Ear3 emits exactly four webhook event types today. Subscribe to the ones you care about when you register the endpoint in the dashboard.
event.type | When it fires |
|---|---|
interview.started | Respondent opened the link and the session began |
interview.completed | Respondent finished all questions |
interview.failed | Voice pipeline crashed mid-interview |
session.expired | Link expired before the respondent reached the end |
Handle unknown event.type values gracefully — new types may be added
without a breaking-change release.
ear3.webhooks.constructEvent(rawBody, signature, secret, options?)
import { Ear3, SignatureVerificationError } from '@ear3/server'
try {
const event = ear3.webhooks.constructEvent(
rawBody, // BEFORE JSON.parse — see warning
req.headers['ear3-signature'],
process.env.EAR3_WEBHOOK_SECRET!,
)
} catch (err) {
if (err instanceof SignatureVerificationError) {
return new Response('Invalid signature', { status: 400 })
}
throw err
}Options
| Option | Type | Default | Notes |
|---|---|---|---|
toleranceSeconds | number | 300 | Reject timestamps outside this window |
Throws SignatureVerificationError for:
- Missing or malformed
Ear3-Signatureheader - Computed HMAC doesn’t match any
v1=in the header (constant-time) - Timestamp outside the tolerance window
rawBodyisn’t valid JSON
🛑 Don’t
JSON.parse(rawBody)before passing it in. Re-serializing changes whitespace, which changes the HMAC, which breaks the check. Usereq.text()(Next.js App Router) orexpress.raw({ type: 'application/json' }).
Errors
Ear3Error— non-2xx response from the API. Has.statusand.code.SignatureVerificationError— see above.
Full error catalog is in the Reference → Errors page.
Related
- @ear3/voice-interviewer — browser companion (native RTVI + headless client)
- Webhook events — full event catalog + payload shapes
- Recipes — patterns for common webhook + session flows