Guides · 16 / 42
Twilio
Point your Twilio number's Voice URL and Status Callback at your handler, verify X-Twilio-Signature, call POST /v1/recall_and_enrich on the Voice webhook and POST /v1/calls/end on a terminal status, and keep your key as a server-side secret.
The Northsun Twilio kit is a published integration kit for developers running voice agents directly on raw Twilio — TwiML Voice webhooks and Media Streams — not on a voice-AI platform like Vapi or Retell. Twilio has no assistant concept, no vendor prompt field, and no LLM of its own, so this kit does not build a response for you: it resolves Northsun context for the caller and hands it back so your own TwiML/IVR/LLM code decides what to do with it. 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 Twilio's documented webhook contract — not a native Twilio feature or a certified Twilio adapter.
If you're on Vapi, Bland, Retell, ElevenLabs, or another voice-AI platform that already wraps Twilio for you, use that platform's own kit instead — this page is for teams wiring TwiML or Media Streams directly.
Set your key
NORTHSUN_API_KEY=sk_live_...
NORTHSUN_BASE_URL=https://mcp.northsun.ai # set it explicitly; see below
TWILIO_AUTH_TOKEN=... # Console -> Account -> API keys & tokens, 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.
TWILIO_AUTH_TOKEN is your Twilio Account Auth Token — not the Account SID — used to verify X-Twilio-Signature on both webhooks below.
The two entry points
Twilio-direct has two call shapes, not one:
- Path A — the Voice URL webhook. Twilio POSTs to your phone number's (or TwiML App's) configured Voice URL on an inbound call, or when an outbound call you placed needs TwiML.
handleTwilioVoiceWebhook()verifiesX-Twilio-Signature, resolves Northsun context for the caller, and returns it — your handler still owes Twilio a TwiML response; this kit does not build one. - Path B — outbound calls your own server places. There is no Twilio-initiated webhook here — your code calls the Twilio REST API (
Calls.create) to start the call, so it fetches Northsun context itself, inline, beforehand.buildTwilioSessionContext()answers this path.
Path A Voice URL webhook -> verify X-Twilio-Signature, POST /v1/recall_and_enrich, build your own TwiML
Path B your outbound call -> POST /v1/recall_and_enrich before Calls.create()
Status terminal CallStatus -> POST /v1/calls/endWire the webhook
In your Twilio Console, set the phone number's (or TwiML App's) Voice URL to your deployed handler, and its Status Callback URL to the same worker on a different path so handleTwilioStatusCallback() can write completed calls back:
Voice URL = https://your-worker.example.com/twilio/voice
Status Callback URL = https://your-worker.example.com/twilio/status-callbackTwilio POSTs application/x-www-form-urlencoded, not JSON — parse the form body and pass every field to the kit as Record<string, string>.
What you get back — and what you don't
Unlike the voice-AI-platform kits, this kit returns no vendor response envelope — Twilio has no equivalent concept for a raw Voice webhook. There is no TwiML either: your code decides which TwiML to return. Use a constant opening greeting, and pass the memory block separately to your model call. ctx.variables carries the six host-safe fields (is_returning, history_known, caller_known, has_memory_facts, line_type and memory_status), and ctx.system_prompt is ready to hand to your own LLM/IVR logic. 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). It is returned on ctx.memory_block; keep it out of the system prompt and send it as a separate non-system message.
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 your prompt logic 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.
Signature verification
handleTwilioVoiceWebhook() never throws on a bad signature — it falls back to a safe default context shaped identically to a genuine unknown caller. Check ctx.signature_status before trusting the result: "verified", "unverified" (a token was configured and the check failed — treat as an attack, not a cache miss), or "skipped" (no token configured). Every shipped example returns HTTP 403 on "unverified" before building any TwiML.
handleTwilioStatusCallback() has a stricter posture: it refuses ({ ok: false }) without calling Northsun on a bad signature, since a forged write to /v1/calls/end is a data-integrity concern.
If your deployment sits behind a proxy or tunnel, verify the URL your handler reconstructs matches, byte-for-byte, what Twilio actually signed — Twilio strips ports and userinfo before signing, and a rewritten scheme/host or an added trailing slash will fail every check even for genuine Twilio traffic.
Post-call write-back
handleTwilioStatusCallback() only writes back to Northsun on a terminal CallStatus (completed, busy, failed, no-answer, canceled) — every other status transition (queued, initiated, ringing, in-progress) returns { ok: true, skipped: true } without calling Northsun, so you can point the same URL at every status event. Twilio's Status Callback carries no transcript field — Twilio does not run ASR on this path — so pass your own transcript (from Media Streams + your own STT, if you built one) via the transcript option, or omit it; omitted is honest, not a bug.
This kit has no per-call cache and Twilio's Status Callback has no field for the context_audit_id the other kits echo back automatically. When the context result carries a context_audit_id (it is present only when the voice feedback loop is enabled for your deployment), store it yourself (keyed by CallSid) when you get it from the Voice webhook handler or the outbound session-context builder, and pass it back as contextAuditId when you call handleTwilioStatusCallback().
Verify it
Call the Twilio number pointed at your Voice URL. Your worker logs should show one recall on the Voice webhook, and one POST /v1/calls/end when Twilio's Status Callback fires with a terminal status. 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
- ElevenLabs, Retell, LiveKit — the same lifecycle on higher-level voice-AI platforms, if raw TwiML isn't what you're building against.
- Voice integration patterns — the platform-neutral lifecycle this kit implements.
- Production readiness — the launch checklist before you point this at real traffic.