Guides · 17 / 42
ElevenLabs
Point your agent's conversation-initiation and post-call webhooks at your worker, call POST /v1/recall_and_enrich before the agent speaks and POST /v1/calls/end after, and keep your key as a server-side secret.
The Northsun ElevenLabs kit is a 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. The kit is published on npm under its current scope, and the versions there are older than the behaviour described on this page, which is the kit's current source; the next release ships under the Northsun package name (in progress). Until then, the two HTTP calls documented on this page work without the kit, as shown on Voice integration patterns; the contact form can supply the current package name. This is a Northsun-built, Northsun-maintained kit against ElevenLabs' documented webhook contract — not a native ElevenLabs feature or a certified ElevenLabs integration.
Set your key
NORTHSUN_API_KEY=sk_live_...
NORTHSUN_BASE_URL=https://mcp.northsun.ai # set it explicitly; see below
ELEVENLABS_INITIATION_SECRET=... # shared-secret header value, recommended
ELEVENLABS_POST_CALL_SECRET=... # HMAC signing secret from the ElevenLabs dashboard, recommendedNORTHSUN_API_KEY is your Northsun API key — see Installation for how to get one. Set it as a platform secret (wrangler secret put NORTHSUN_API_KEY on Cloudflare Workers, or your platform's equivalent), never as a plaintext environment value in a repo or client bundle.
These are names you choose. Pass the key and the base URL https://mcp.northsun.ai to the client's options explicitly: the kit reads no environment itself, and the published client defaults to an earlier hostname that serves the same surface.
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.
Path A conversation-initiation webhook -> POST /v1/recall_and_enrich, return client_data
Path B your own outbound / register_call / SDK start -> POST /v1/recall_and_enrich inline
Post post_call_transcription webhook -> verify the HMAC, POST /v1/calls/endWire 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-callRequired 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.
What you get back
variables carries the six host-safe fields (is_returning, history_known, caller_known, has_memory_facts, line_type and memory_status), also projected into client_data.dynamic_variables (nulls coalesced to "", since ElevenLabs' dynamic_variables only support string | number | boolean). Caller-derived text (name, company, summary, facts) is not exposed as variables: a stored value would be interpolated into your agent's prompt outside the quoted data block. It reaches the model only inside the quoted memory block (memory_block). system_prompt is the same text embedded at client_data.conversation_config_override.agent.prompt.prompt; the ElevenLabs prompt override is the one channel the memory block travels in, so the kit appends it there, after every instruction. A prompt you build from the variables alone has no memory block and drops the caller's name, history and facts. The first message is always "Hi there, how can I help?". client_data is the literal wire body — return it directly from a Path A handler, or embed it in a Path B call.
Drop removed dynamic variable definitions from existing agents: ElevenLabs requires every defined variable to be supplied.
memory_status is a tri-state, because an empty memory block 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. The returned facts are 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). The facts list may be empty here too, but it 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 the Vapi and Bland kits (one HMAC gate each), this kit authenticates its two webhooks differently:
| Webhook | Mechanism |
|---|---|
| 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.
Closing the attribution loop
context_audit_id (present only when the voice feedback loop is enabled for your deployment) and a sealed caller handle are stashed as two reserved, prefixed custom dynamic_variables at initiation and read back off data.conversation_initiation_client_data.dynamic_variables in the post-call handler. ElevenLabs' documented post_call_transcription payload shape echoes the initiation dynamic_variables back; that is resolved at the docs and schema level, and a real inbound call against a deployed endpoint is still the falsifier that confirms it against live traffic.
The phone is never a host variable. A caller phone is caller- or provider-controlled (caller-ID spoofing), and a dynamic variable can be templated into your agent's privileged prompt, so the handle does not carry it: it carries the canonical E.164 number sealed with AES-256-GCM under a key derived from your secret. A caller cannot choose a byte of it, and a forged or tampered handle fails authentication on the way back. A phone that is not canonical E.164 is dropped, not truncated: no handle, recall still runs, and the post-call write-back for that call needs the phone number your own server knows.
The sealing secret is a high-entropy value you configure (for example from openssl rand -base64 32; never a short or guessable phrase), falling back to the post-call signing secret. Use the same value on the pre-call and post-call side. With neither configured the kit seals no handle, ignores any handle in a post-call body, and write-back needs an explicit phone number. An unsigned post-call body therefore cannot name an arbitrary phone to write memory against.
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 the 204 confirms and how to verify the write.
Next steps
- Twilio, Retell, LiveKit — the same lifecycle on other platforms.
- Voice integration patterns — the platform-neutral lifecycle this kit implements.
- Production readiness — the launch checklist before you point this at real traffic.