Guides · 19 / 42
LiveKit
Call the kit's context fetch after ctx.connect(), pass the instructions to your voice agent, and write back from ctx.addShutdownCallback(); keep your key as a server-side secret.
The Northsun LiveKit kit is a published integration kit for LiveKit Agents: direction-safe SIP caller-identity resolution, a single Northsun context-fetch call for voice.Agent({ instructions }), and session-end memory write-back via ctx.addShutdownCallback(). 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 same two HTTP calls (POST /v1/recall_and_enrich, POST /v1/calls/end) 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 LiveKit's documented SIP participant and job-context contract — not a native LiveKit feature or a certified LiveKit integration.
Why this kit looks different from the webhook kits
Every other Northsun voice kit (Twilio, ElevenLabs, Retell, Vapi, Bland) is built around a webhook: the vendor's platform POSTs to an HTTP endpoint you control. LiveKit Agents has no such thing. A LiveKit Agent is a worker process that connects outward to the LiveKit server (ctx.connect()), authenticated at the SDK/transport level — there is no vendor-signed inbound request for this kit to receive or verify, and no signature.ts in this kit for that reason. Your own worker code is always the one that calls Northsun, for both inbound and outbound telephony — fetchNorthsunContext() is the single entry point for both directions.
Set your key
NORTHSUN_API_KEY=sk_live_...
NORTHSUN_BASE_URL=https://mcp.northsun.ai # set it explicitly; see belowNORTHSUN_API_KEY is your Northsun API key — see Installation for how to get one. Set it as a platform secret in whatever process manager runs your LiveKit worker, never as a plaintext environment value in a repo or client bundle. LiveKit connectivity itself (LIVEKIT_URL / LIVEKIT_API_KEY / LIVEKIT_API_SECRET) is handled entirely by the LiveKit SDK — this kit never touches those.
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.
Wire it into your worker
after ctx.connect() + ctx.waitForParticipant() -> fetch Northsun context (POST /v1/recall_and_enrich)
new voice.Agent({ instructions }) -> pass the returned instructions
ctx.addShutdownCallback() -> write the session back (POST /v1/calls/end)Direction-safe caller identity
LiveKit documents its SIP participant attributes as naming the customer's phone number the same way in both call directions:
| Attribute | Inbound | Outbound |
|---|---|---|
sip.phoneNumber | the caller (customer) | the number dialed by the SIP participant (customer) |
sip.trunkPhoneNumber | the DID dialed into (your own) | the trunk's own originating number (your own) |
Per LiveKit's SIP participant documentation, sip.phoneNumber names the customer in both directions; a real outbound call against your own deployment is still the falsifier that confirms it for you. resolveLiveKitCallerIdentity() computes direction explicitly (via sip.ruleID) and returns the customer's number, never the trunk's own number, and fixture tests cover both directions.
Resolution states — read this before you read known: false
fetchNorthsunContext() never throws. Its result carries resolution, one of three states:
"ok"— recall was attempted and Northsun answered.known: falsehere is usually the ordinary "new caller" case."identity_unresolved"— this kit could not determine an E.164 phone number for the participant at all (missing/malformed SIP attributes, or a non-SIP session with no override). Northsun was never called. This is a LiveKit-side configuration problem, not a statement about the caller, and is logged aterrorlevel precisely so it doesn't get silently read as "new caller.""recall_failed"— an E.164 number was resolved, but the recall call itself errored or timed out.
handleLiveKitSessionEnd() refuses ({ ok: false }, Northsun never called) rather than writing an unattributable call when either the phone number or the session ID cannot be resolved — logged loudly, not silently defaulted.
What you get back
fetchNorthsunContext()returns.variables(the six host-safe fields:is_returning,history_known,caller_known,has_memory_facts,line_typeandmemory_status),.instructions(ready to pass directly asnew voice.Agent({ instructions })— named to match LiveKit's own field, not the sibling kits'system_prompt),.memory_block(served caller strings quoted as data), and a metadata object (trace ID,context_audit_idwhen the voice feedback loop is enabled for your deployment, resolution state, direction, timing).handleLiveKitSessionEnd()returns{ ok: boolean; error?: string }.resolveLiveKitCallerIdentity(attributes)returns the raw identity-resolution result if you want to make your own routing decisions before calling Northsun.mapChatHistoryToTranscript(session.history.items)converts LiveKit'sChatContextitems into the transcript shapehandleLiveKitSessionEndexpects.
Caller-derived text is not in the instructions: a stored value would be interpolated into your agent's prompt outside the quoted data block. Put memory_block.text in the chat context as a separate non-system message or tool result. The kit's seed-message helper marks that message so mapChatHistoryToTranscript() never maps it, which keeps the recalled memory block out of write-back; a hand-written user message carries no marker and would be written back as something the caller said.
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.buildInstructions()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.
This kit does not build a voice.Agent for you, does not choose your STT/LLM/TTS providers, and does not decide your call outcome — it hands you enriched context and writes back what you tell it, nothing more. See What recall gives you back for the full field reference.
No vendor SDK at runtime
@livekit/agents / @livekit/rtc-node are devDependencies only — used to verify this kit's hardcoded assumptions (ParticipantKind.SIP, ChatMessage's shape) against the real, installed packages, and in the examples. The kit's own runtime code takes plain Record<string, string> attributes and duck-typed chat items; it never imports @livekit/agents itself, so it has no exposure to that SDK's own churn.
Verify it
Place or receive a call through your deployed LiveKit worker. Your worker logs should show one fetchNorthsunContext() call after ctx.waitForParticipant(), and one handleLiveKitSessionEnd() call from the shutdown callback. See Hangup, and what happens next for what the 204 confirms and how to verify the write. A live-account call against a deployed endpoint is still owed for this kit; treat your own test call as the proof.
Next steps
- Twilio, ElevenLabs, Retell — the same lifecycle on webhook-based platforms.
- Voice integration patterns — the platform-neutral lifecycle this kit implements.
- Production readiness — the launch checklist before you point this at real traffic.