npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@mnemix-ai/twilio-kit

v0.1.0

Published

Mnemix integration kit for developers running voice agents directly on Twilio (TwiML Voice webhooks / Media Streams) — Voice-webhook enrichment on the inbound-call path, an inline session-context builder for outbound calls, X-Twilio-Signature verification

Readme

@mnemix-ai/twilio-kit

Mnemix integration kit for developers running voice agents directly on Twilio — raw TwiML Voice webhooks and Media Streams, not a higher-level voice-AI platform. Voice-webhook enrichment on the inbound-call path, an inline session-context builder for outbound calls, X-Twilio-Signature verification on both webhook entry points, and post-call memory write-back on the Status Callback webhook.

Package scope note: this kit is published as @mnemix-ai/twilio-kit for now, same provisional scope as its siblings (vapi-kit, bland-kit, elevenlabs-kit). @northsun-ai/core is the ratified target scope but is currently a phantom — declared as a dependency elsewhere in this repo, published by nothing. The scope string is isolated to this file's install line, package.json's name field, and the @mnemix-ai/client peerDependency only.

Why this is different from the other kits

Vapi, Bland, and ElevenLabs are voice-AI platforms — each has its own assistant/agent configuration, its own vendor-specific webhook response envelope, and its own LLM running the conversation. Twilio-direct is raw telephony: no agent concept, no vendor prompt field, no LLM of its own. If you're on Twilio's TwiML Voice webhooks or Media Streams and wiring your own LLM/IVR logic, this kit is for you — it hands your code the same enriched context the other kits build, but stops short of building a response envelope for you, because Twilio has no such envelope to build. See "What you get back" for the exact scope boundary.

This kit has two entry points, mirroring the two-path shape of elevenlabs-kit:

  • Path A — the Voice URL webhook. Twilio POSTs to your phone number's (or TwiML App's) configured Voice URL when an inbound call arrives, or when an outbound call you placed needs TwiML. handleTwilioVoiceWebhook() answers this path: verifies X-Twilio-Signature, resolves Mnemix context for the caller, and returns it for your own code to act on — 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 Mnemix context itself, inline, beforehand. buildTwilioSessionContext() answers this path, same shape as buildElevenLabsSessionContext().

A verified, signature-checked Status Callback webhook (handleTwilioStatusCallback()) writes the completed call back to POST /v1/calls/end on a terminal CallStatus.

This is a custom integration kit, not a native or certified Twilio adapter, and it does not claim to be one.

Install

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

Quickstart: Cloudflare Worker

import { Mnemix } from "@mnemix-ai/client";
import { handleTwilioVoiceWebhook, handleTwilioStatusCallback } from "@mnemix-ai/twilio-kit";

interface Env {
  MNEMIX_KEY: string;
  MNEMIX_BASE_URL?: string;
  TWILIO_AUTH_TOKEN?: string;
}

export default {
  async fetch(req: Request, env: Env): Promise<Response> {
    if (req.method !== "POST") return new Response("Method not allowed", { status: 405 });

    const url = new URL(req.url);
    const mnemix = new Mnemix({ apiKey: env.MNEMIX_KEY, baseUrl: env.MNEMIX_BASE_URL || undefined });
    const opts = { mnemix, twilioAuthToken: env.TWILIO_AUTH_TOKEN };
    const signature = req.headers.get("x-twilio-signature") ?? undefined;

    if (url.pathname === "/twilio/voice") {
      const form = await req.formData();
      const params: Record<string, string> = {};
      for (const [k, v] of form.entries()) if (typeof v === "string") params[k] = v;

      const ctx = await handleTwilioVoiceWebhook(opts, params, { url: req.url, signature });

      // Reject a forged request explicitly — a failed signature still
      // returns a normal-shaped context (see "Signature verification"
      // below), so don't fall through to 200 TwiML unconditionally.
      if (ctx.signature_status === "unverified") return new Response("Forbidden", { status: 403 });

      const name = ctx.variables.caller_name.replace(/[<>&'"]/g, ""); // XML-escape before interpolating — see "What you get back"
      const twiml = `<?xml version="1.0" encoding="UTF-8"?><Response><Say>Welcome back, ${name}.</Say></Response>`;
      return new Response(twiml, { headers: { "Content-Type": "text/xml" } });
    }

    if (url.pathname === "/twilio/status-callback") {
      const form = await req.formData();
      const params: Record<string, string> = {};
      for (const [k, v] of form.entries()) if (typeof v === "string") params[k] = v;

      const result = await handleTwilioStatusCallback(opts, params, { url: req.url, signature });
      return Response.json(result, { status: result.ok ? 200 : 400 });
    }

    return new Response("Not found", { status: 404 });
  },
};

See examples/cloudflare-worker.ts for the full working file (proper XML-escaping, error handling, and a Path B startOutboundTwilioCall example).

Quickstart: Node Express

See examples/express-server.ts. Uses express.urlencoded({ extended: false }) — Twilio POSTs application/x-www-form-urlencoded, not JSON.

Quickstart: Next.js App Router

See examples/nextjs-route.ts — a single app/api/twilio/[hook]/route.ts dispatching on params.hook.

The two entry points

handleTwilioVoiceWebhook() — Path A (webhook)

Call this from the HTTP handler behind the URL you configure as the phone number's (or TwiML App's) Voice URL. Authenticated by X-Twilio-Signature — Twilio's own documented HMAC-SHA1 scheme (see Signature verification below).

const ctx = await handleTwilioVoiceWebhook(
  { mnemix, twilioAuthToken: process.env.TWILIO_AUTH_TOKEN },
  params, // the FULL parsed form body — TwilioVoiceWebhookPayload or Record<string, string>
  { url: exactRequestUrl, signature: receivedSignatureHeader, tone: "warm" },
);
// ctx.variables / ctx.system_prompt are yours to use however your call-
// handling code needs — build TwiML, wire Media Streams into your own LLM,
// whatever fits your application. This kit does not decide that for you.

buildTwilioSessionContext() — Path B (inline)

Call this from your own server code, right before you place an outbound call via the Twilio REST API. No signature step — there is no inbound request to authenticate, because your own server is what is about to initiate.

const ctx = await buildTwilioSessionContext({ mnemix }, { phone_number: "+15551234567" });
// use ctx.system_prompt / ctx.variables in your own outbound-call wiring,
// and stash ctx.mnemix.context_audit_id somewhere you can read it back at
// hangup (see "Closing the attribution loop" below).

Both return the same TwilioPreCallContext shape.

What you get back

interface TwilioPreCallContext {
  variables: {
    caller_name: string;
    is_returning: boolean;
    history_known: boolean; // AGE-829 — see below
    last_intent: string | null;
    last_call_summary: string | null;
    carrier: string | null;
    line_type: string | null;
    company: string | null;
    role: string | null;
    industry: string | null;
    suggested_intent: string | null;
    memory_facts?: string[];
  };
  system_prompt: string; // hand this to your own LLM/agent logic
  signature_status: "verified" | "unverified" | "skipped"; // see "Signature verification" below
  mnemix: {
    trace_id: string;
    known: boolean;
    caller_id: string;
    memory_age_ms: number;
    timing_ms: { total: number; memory_ms: number; enrichment_ms: number };
    context_audit_id?: string | null;
  };
}

No client_data, no TwiML envelope. Unlike elevenlabs-kit, this kit does not build a vendor response body — Twilio has no equivalent concept for a raw Voice-webhook handler. Your own code decides what TwiML to return (a <Say>, a <Dial> into Media Streams wired to your own LLM, a <Gather>, whatever your application needs) using ctx.variables / ctx.system_prompt. ctx.variables.caller_name (and the other free-text fields) are not XML-escaped or prompt-sanitized on their own — only ctx.system_prompt runs the sanitizer (see below). Escape them yourself before interpolating into TwiML XML (see examples/*.ts's escapeXml helper).

history_known (AGE-829): is_returning is a boolean and cannot express "we don't know." When the caller's total-call count is unavailable and no calls are cached, history_known is false so your prompt logic can ask rather than wrongly announcing a first-time caller by name.

System Prompt Customization

import { buildSystemPrompt } from "@mnemix-ai/twilio-kit";

const systemPrompt = buildSystemPrompt(ctx.variables, {
  tone: "warm", // "warm" | "professional" | "casual"
  agentName: "Avery",
  brandName: "Northstar Support",
  fallbackGreeting: "Hi there",
});

The builder does not invent missing history. If Mnemix returns no memory, the prompt tells your agent to ask rather than assume — and every Mnemix-sourced string (caller name, company, role, prior summary, governed facts) passes through the same prompt-injection sanitizer used by vapi-kit, bland-kit, and elevenlabs-kit (hardened 2026-08-18) before it reaches the prompt text. That sanitizer is reused verbatim here, not reimplemented.

Signature verification (X-Twilio-Signature)

One auth mechanism, reused on both webhooks — unlike elevenlabs-kit's two different mechanisms, Twilio uses the same scheme for every webhook it signs.

Algorithm (VERIFIED against Twilio's own docs, https://www.twilio.com/docs/usage/security, fetched 2026-08-18): sort the received form-parameter keys ordinally, concatenate key + value for each onto the full request URL (including query string), HMAC-SHA1 the result with your Account Auth Token, base64-encode, and compare against the X-Twilio-Signature header. See src/signature.ts for the full citation and a golden-vector test reproducing Twilio's own documented worked example byte-for-byte — including the exact signature Twilio's docs publish for AuthToken 12345, independently re-derived via node:crypto before being pinned as a literal.

Getting the URL right behind a proxy. The signed URL must match, byte-for-byte, the URL Twilio actually signed — which is not always the URL you configured, because Twilio itself normalizes it first (VERIFIED, same docs page):

  • Scheme and host must match what's configured on the Twilio phone number / TwiML App. A load balancer, tunnel, or reverse proxy that rewrites the scheme (http vs https) or Host header before your handler sees the request will make every signature check fail even for genuine Twilio traffic.
  • Port and userinfo are dropped. For HTTPS voice callbacks, Twilio strips any user:pass@ prefix and any explicit port number before signing — https://user:[email protected]:8443/voice is signed as https://example.com/voice. If your deployment is reachable on a non-default port (common behind a tunnel, or a non-443 Express deploy) and your handler's reconstructed URL includes that port, every check fails.
  • Trailing slashes matter. A proxy or web server (Apache/PHP rewrites are the documented example) that appends a / to the configured URL before your app sees it produces a different string than what Twilio signed.

Verify what URL your deployment actually reconstructs (req.url in a Worker, req.protocol + req.get('host') + req.originalUrl in Express with trust proxy configured correctly) against what Twilio actually signed — not just what you configured — before relying on this in production. See src/signature.ts's file header for the full citation.

The verification verdict is observable — check it, don't assume 200 means safe. If twilioAuthToken is configured, handleTwilioVoiceWebhook() falls back to a safe default context (never throws) on a missing/invalid signature — a voice call should not crash on a bad signature — but that default context is shaped identically to a genuine unknown caller. Check ctx.signature_status before trusting the result or returning a normal response: "verified" (checked out), "unverified" (a token WAS configured and the check FAILED — Twilio's own security posture is to reject a forged request, so treat this as an attack, not a cache miss), or "skipped" (no token configured at all — the deliberate unauthenticated opt-out). Every example in this kit returns HTTP 403 on "unverified" before building any TwiML — see examples/*.ts.

handleTwilioStatusCallback() has a stricter posture: it refuses ({ ok: false }) without calling Mnemix on a bad signature, matching elevenlabs-kit's post-call posture — a forged write to /v1/calls/end is a data-integrity concern, not something to silently paper over with defaults.

verifyTwilioSignature() and computeTwilioSignature() are exported directly if you want to build your own policy on top of the raw verdict.

JSON-body webhooks (bodySHA256) — out of scope for this kit's own two entry points, but handled if you call verifyTwilioSignature() directly against one. Twilio's Voice and Status Callback webhooks (the ones handleTwilioVoiceWebhook/handleTwilioStatusCallback target) are always application/x-www-form-urlencoded — never JSON — so this case never arises through this kit's own two functions. If you use the exported verifyTwilioSignature() against some other Twilio webhook that Twilio delivers as JSON, pass the raw request body as rawBody: Twilio signs a bodySHA256 query parameter on the URL instead of form params in that case, and without rawBody to check it against, a captured valid signed URL could be replayed with a different JSON body and still verify. verifyTwilioSignature() fails closed (returns false) if the URL carries bodySHA256 and no rawBody was given — see src/signature.ts's file header for the full citation.

Graceful Degradation

Voice calls should not block on enrichment. If Mnemix times out (default 600ms, recallTimeoutMs), returns a 5xx, or the caller's phone number is missing/malformed, handleTwilioVoiceWebhook() and buildTwilioSessionContext() return safe defaults — caller_name: "there", is_returning: false, history_known: false — instead of throwing. Your call-handling code can still proceed; the failure is logged, not surfaced to the caller.

Post-call write-back — handleTwilioStatusCallback()

Twilio's Status Callback fires once per call-status transition (queuedinitiatedringingin-progress → a terminal status) — but Twilio may also retry delivery of any individual request (configurable retry count, I-Twilio-Idempotency-Token header) if your endpoint doesn't acknowledge it, so "once per transition" describes the intended sequence, not a delivery guarantee. This handler only writes back to Mnemix on a terminal CallStatus (completed, busy, failed, no-answer, canceled) — every other invocation returns { ok: true, skipped: true } without calling Mnemix, so you can point the same Status Callback URL at every event without creating a write per ring. A retried terminal-status delivery is safe to call again: Mnemix's /v1/calls/end dedupes on session_id (the CallSid this kit passes), so a duplicate delivery of the same terminal event is a no-op server-side, not a duplicate interaction record.

const result = await handleTwilioStatusCallback(
  { mnemix, twilioAuthToken },
  params,
  {
    url, signature,
    // Twilio's Status Callback payload carries NO transcript field — Twilio
    // does not perform ASR on this path, unlike Vapi/Bland/ElevenLabs. If
    // your own application assembled one (Media Streams + your own STT, or
    // your own LLM pipeline), pass it here; omitted is honest, not a bug.
    transcript: myOwnTranscript,
    contextAuditId: myStoredContextAuditId, // see below
  },
);

CallStatus values don't map cleanly onto Mnemix's outcome enum. Only no-answer has a 1:1 equivalent; everything else defaults to other unless you pass a valid outcome override.

Closing the attribution loop

This kit has no per-call cache and Twilio's Status Callback payload has no field for context_audit_id (the feedback-addressable packet id /v1/recall_and_enrich returns). Unlike elevenlabs-kit, which closes this loop via a documented dynamic_variables echo, Twilio-direct has nothing equivalent to echo through. You own carrying context_audit_id forward yourself — store it (e.g. keyed by CallSid, in your own DB or KV) when you get it from handleTwilioVoiceWebhook's / buildTwilioSessionContext's returned ctx.mnemix.context_audit_id, then pass it back as StatusCallbackOptions.contextAuditId when you call handleTwilioStatusCallback.

Errors

TwilioKitError is the base typed error. TwilioKitSignatureError represents the signature-verification failure mode. Both entry points are designed to degrade safely rather than throw these — they exist for callers who want a typed error to map to their own policy.

Configuration

| Option | Required | Description | | --- | --- | --- | | mnemix | Yes | A Mnemix client constructed with your API key. | | twilioAuthToken | No, recommended | Twilio Account Auth Token (Console → Account → API keys & tokens — NOT the Account SID). Used to verify X-Twilio-Signature on both webhooks. | | recallTimeoutMs | No (default 600) | Maximum time to wait before falling back to default context. | | logger | No | Logger with info, warn, error; defaults to console. |

Public API Surface

  • POST /v1/recall_and_enrich for pre-call voice memory and enrichment.
  • POST /v1/calls/end for post-call memory write-back.

The only other frozen public v1 endpoint is GET /v1/caller/{phone_number}; this kit does not call it.

Construct the client with baseUrl: process.env.MNEMIX_BASE_URL || undefined when you need an explicit origin. Empty or omitted values let @mnemix-ai/client use its default.

What's proven, and what's still owed

This kit's contract tests (fixture-driven, no Twilio account required) prove: the full 12-field variable mapping, the AGE-829 three-state branch, the prompt-injection sanitizer (byte-identical to the sibling kits'), the X-Twilio-Signature algorithm against a golden vector reproduced from Twilio's own published docs (including a pinned literal signature, not just a shape check), the bodySHA256 JSON-body case (accepts a matching body, rejects a missing or tampered one), terminal-vs-non-terminal CallStatus gating, direction-aware customer-phone resolution on BOTH webhooks (handleTwilioVoiceWebhook reads To — not From — for Direction: "outbound-api"/"outbound-dial", with a regression test asserting the tenant's own Twilio number is never enriched), the signature_status field across all three states, and that a hung recall yields defaults and never throws.

Owed — needs a real Twilio account, named individually with their arming condition in test/live-account.test.ts (all it.skip, none mocked): that the Voice webhook actually fires and reaches a deployed handler; that the resolved context genuinely reflects a seeded caller's Northsun memory on a real call, not just a fixture; that a captured real X-Twilio-Signature header verifies against this kit's implementation, not just against its own independently-built test fixtures; that the Status Callback fires on a real hangup and the write-back is confirmed via a later recall.

Claims status

Twilio-direct is not yet an advertised integration anywhere in Northsun/Mnemix's public copy. scripts/aeo-audit.ts's claim-surface check treats any unqualified mention of "Twilio" (not immediately followed by "Lookup") as a forbidden claim on public surfaces — this kit ships code-first, with no public docs page, web/ copy, or claims-registry edit, per the internal-law decision at docs/decisions-log/2026-08-18-officially-supported-tier.md. That entry currently records Vapi and Bland as the only Tier-1 "Officially Supported" candidates (both blocked on a separate peer-dependency publish issue); Twilio-direct enters the same two-tier framework — Tier 2 ("works via the API," raw v1 routes, no maintained-kit guarantee) today, with Tier 1 eligibility requiring the same conjunctive checklist (tests + fixtures on origin/main, clean public-registry install, a named owner, a drift probe) plus founder ratification of the tier language itself before any public surface may use it.

Privacy & Compliance

The kit requires E.164 caller numbers and redacts phone numbers in its own logs. Twilio never sends this kit a transcript — if your own application assembles one (Media Streams + your own speech-to-text, or your own LLM pipeline) and forwards it to Mnemix via handleTwilioStatusCallback's transcript option, redact or filter that data per your own consent, retention, and data-processing terms before you do.

License

MIT