Documentation

ElevenLabs

@mnemix-ai/elevenlabs-kit is a real, published integration kit for ElevenLabs Agents: conversation-initiation enrichment on the inbound-phone path, an inline session-context builder for outbound / Register Call / SDK sessions, and HMAC-verified post-call memory write-back. It calls the same two frozen routes documented on Voice integration patterns — POST /v1/recall_and_enrich before the call, POST /v1/calls/end after — and nothing else. This is a Northsun-built, Northsun-maintained kit against ElevenLabs' documented webhook contract — not a native ElevenLabs feature or a certified ElevenLabs integration.

Install

npm install @mnemix-ai/elevenlabs-kit @mnemix-ai/client

Verified in a clean directory: @mnemix-ai/elevenlabs-kit@0.1.0's peer dependency on @mnemix-ai/client@^0.2.2 resolves against the published @mnemix-ai/client@0.2.2, install exits 0, and both packages import clean.

@mnemix-ai/* is today's published scope. If your integration will outlive a possible future move to @northsun-ai/*, pin the version above rather than tracking latest.

Set your key

MNEMIX_KEY=sk_live_...
MNEMIX_BASE_URL=https://mcp.mnemix.ai         # optional; this is the default
ELEVENLABS_INITIATION_SECRET=...              # shared-secret header value, recommended
ELEVENLABS_POST_CALL_SECRET=...               # HMAC signing secret from the ElevenLabs dashboard, recommended

MNEMIX_KEY is your Northsun API key — see Installation for how to get one. Set it as a platform secret (wrangler secret put MNEMIX_KEY on Cloudflare Workers, or your platform's equivalent), never as a plaintext environment value in a repo or client bundle.

Two ways a conversation starts, one kit

ElevenLabs Agents has four ways a conversation can begin, but only one gets an ElevenLabs-initiated webhook before the agent speaks:

  • Path A — native-Twilio-imported inbound numbers, and inbound SIP-trunk calls. ElevenLabs calls your configured conversation-initiation webhook (ElevenLabs' docs also call this "Twilio personalization" or the "pre-call webhook") before the agent's first utterance. handleElevenLabsConversationInitiation() answers this path.
  • Path B — native outbound calls, the advanced Register Call route on your own Twilio infrastructure, and SDK/widget session starts. There is no ElevenLabs-initiated hook here — your own code starts the session, so it fetches Northsun context itself, inline. buildElevenLabsSessionContext() answers this path.
  • The web widget (Path C) is out of scope for v1 — there is no phone number, and Northsun recall is phone-keyed.
import { Mnemix } from "@mnemix-ai/client";
import { handleElevenLabsConversationInitiation, buildElevenLabsSessionContext } from "@mnemix-ai/elevenlabs-kit";

const mnemix = new Mnemix({ apiKey: env.MNEMIX_KEY, baseUrl: env.MNEMIX_BASE_URL });
const opts = {
  mnemix,
  initiationSharedSecret: env.ELEVENLABS_INITIATION_SECRET,
  postCallWebhookSecret: env.ELEVENLABS_POST_CALL_SECRET,
};

// Path A — conversation-initiation webhook:
const ctx = await handleElevenLabsConversationInitiation(opts, payload, { receivedSecret, tone: "warm" });
return Response.json(ctx.client_data); // the literal ElevenLabs response envelope

// Path B — your own outbound call / register_call / SDK session start:
const outboundCtx = await buildElevenLabsSessionContext({ mnemix }, { phone_number: "+15551234567" });

See packages/elevenlabs-kit/examples/cloudflare-worker.ts for the full working file, or examples/express-server.ts / examples/nextjs-route.ts for the same two routes on other runtimes.

Wire the webhooks

In your ElevenLabs agent's dashboard configuration, set the conversation-initiation webhook and the post-call webhook:

Conversation-initiation webhook = https://your-worker.example.com/elevenlabs/conversation-initiation
Post-call webhook               = https://your-worker.example.com/elevenlabs/post-call

Required agent configuration — do this or nothing renders. ElevenLabs overrides are opt-in per field, off by default. If the agent's dashboard config doesn't have "system prompt override" and "first message override" explicitly enabled, client_data.conversation_config_override is silently ignored — undetectable from this kit's side. See packages/elevenlabs-kit/examples/elevenlabs-agent-config.json for the exact dashboard fields.

What you get back

variables carries the same 13-field shape every sibling kit returns (caller_name, is_returning, history_known, last_intent, last_call_summary, carrier, line_type, company, role, industry, suggested_intent, memory_facts, memory_status), also projected into client_data.dynamic_variables (nulls coalesced to "", since ElevenLabs' dynamic_variables only support string | number | boolean). system_prompt is the same text embedded at client_data.conversation_config_override.agent.prompt.prompt. client_data is the literal wire body — return it directly from a Path A handler, or embed it in a Path B call.

memory_status is a tri-state, because memory_facts alone cannot tell "this caller genuinely has no governed facts" apart from "the facts read failed and we could not check":

  • "ok" — the read ran and answered. memory_facts is a real measurement, possibly empty.
  • "unavailable" — the read did not complete (server-side budget or error, or recall itself failed, timed out, or was never reached). memory_facts is [] here too, but that [] is not a measurement. The prompt builder renders an explicit instruction telling the agent not to state or imply the caller has no facts on file, instead of staying silent — silence would let the agent infer "nothing on file" on its own, which is the exact false statement this field exists to prevent.
  • "off" — your tenant deployment has memory-unification reads disabled. Feature off, not a failure; rendered the same as "ok" with an empty list.

Current limitation — read this before you branch on "ok". The tri-state is only as precise as what the recall API puts on the wire, and the server-side change that reports a dropped governed read distinctly is not yet released. Until it is:

  • "ok" under-warns. If the governed-facts read times out or errors for a caller who already has call history, the API still returns an empty list, and it is indistinguishable from a caller who genuinely has no stored facts. The kit reports "ok" and the prompt stays silent.
  • "unavailable" can over-warn. The kit infers it when the caller's interaction count is also unreadable, which is deliberate but is not proof the facts read itself failed. Do not diagnose a facts outage from this field alone.

The direction of both errors is deliberate: the kit would rather warn you about a record it did read than stay silent about one it did not. Treat "unavailable" as "do not assert this caller has nothing on file" rather than as a precise diagnostic, and do not treat "ok" as proof of a measurement until the server change ships. This note will be removed when it does.

history_known distinguishes "we checked and this caller has no history" from "we don't know" — is_returning alone can't express the second case. When the caller's total-call count is unavailable and no calls are cached, history_known is false so the prompt asks rather than wrongly announcing a first-time caller by name.

See What recall gives you back for what populates these fields and when a value is legitimately absent rather than wrong.

Webhook verification — two different mechanisms

Unlike vapi-kit/bland-kit (one HMAC gate each), this kit authenticates its two webhooks differently:

WebhookMechanism
Conversation-initiation (pre-call)Shared-secret request header, timing-safe compare
Post-call (post_call_transcription)HMAC-SHA256, elevenlabs-signature header, t=<unix_seconds>,v0=<hex>

If postCallWebhookSecret is configured, handleElevenLabsPostCall() refuses ({ ok: false }) without calling Northsun when the signature is missing or invalid. If initiationSharedSecret is configured, handleElevenLabsConversationInitiation() falls back to a safe default context (never throws) on a mismatch — ElevenLabs can still start the call; the failure is logged, not surfaced to the caller.

The post-call HMAC algorithm was read directly out of the official @elevenlabs/elevenlabs-js SDK's shipped source, not guessed from prose — see packages/elevenlabs-kit/src/signature.ts's file header for the citation.

Closing the attribution loop

context_audit_id and the caller's own phone number are stashed as reserved, mnemix_-prefixed dynamic_variables at initiation and read back off the post-call payload's echoed dynamic_variables — resolving at the docs/schema level ElevenLabs' documented behavior that post_call_transcription echoes the initiation dynamic_variables back. A real inbound call against a deployed endpoint is still the falsifier that confirms this against live traffic, not just docs (see packages/elevenlabs-kit/README.md's "What's proven, and what's still owed").

Verify it

Call the ElevenLabs agent's connected number. Your worker logs should show one recall on the conversation-initiation webhook, and one POST /v1/calls/end when ElevenLabs' post-call webhook fires. See Hangup, and what happens next for what happens immediately after that call.

Next steps