Twilio
@mnemix-ai/twilio-kit is a real, 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. 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.
Install
npm install @mnemix-ai/twilio-kit @mnemix-ai/client
Verified in a clean directory: @mnemix-ai/twilio-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 trackinglatest.
Set your key
MNEMIX_KEY=sk_live_...
MNEMIX_BASE_URL=https://mcp.mnemix.ai # optional; this is the default
TWILIO_AUTH_TOKEN=... # Console -> Account -> API keys & tokens, 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.
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.
import { Mnemix } from "@mnemix-ai/client";
import { handleTwilioVoiceWebhook, handleTwilioStatusCallback } from "@mnemix-ai/twilio-kit";
const mnemix = new Mnemix({ apiKey: env.MNEMIX_KEY, baseUrl: env.MNEMIX_BASE_URL });
const opts = { mnemix, twilioAuthToken: env.TWILIO_AUTH_TOKEN };
// Path A — your number's Voice URL:
const ctx = await handleTwilioVoiceWebhook(opts, formParams, { url: req.url, signature });
if (ctx.signature_status === "unverified") return new Response("Forbidden", { status: 403 });
// build your own TwiML from ctx.variables / ctx.system_prompt here
// Path B — your own outbound call, right before Calls.create():
const outboundCtx = await buildTwilioSessionContext({ mnemix }, { phone_number: "+15551234567" });
See packages/twilio-kit/examples/cloudflare-worker.ts for the full working file (XML-escaping, error handling, and a Path B startOutboundTwilioCall example), or examples/express-server.ts / examples/nextjs-route.ts for the same two routes on other runtimes.
Wire 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-callback
Twilio 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. ctx.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), and ctx.system_prompt is ready to hand to your own LLM/IVR logic. ctx.variables.caller_name and the other free-text fields are not XML-escaped on their own — only ctx.system_prompt runs the prompt-injection sanitizer. Escape any field you interpolate into TwiML yourself; see the escapeXml helper in examples/*.ts.
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_factsis 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_factsis[]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 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. See packages/twilio-kit/README.md's "Signature verification" section for the full citation and the exact algorithm.
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. Store ctx.mnemix.context_audit_id yourself (keyed by CallSid) when you get it from handleTwilioVoiceWebhook() / buildTwilioSessionContext(), 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 happens immediately after that call.
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.