Voice agent integration · 12 minutes quickstart · Server URL (assistant-request / end-of-call-report messages)

Mnemix + Vapi

Vapi.ai routes every event — inbound call setup, mid-call tool calls, end-of-call reporting — through a single Server URL webhook, branching on message.type. Mnemix slots into that webhook as the memory + identity layer: Vapi's assistant-request message resolves the caller through Twilio Lookup + Trestle before the assistant is even attached to an inbound call, and Vapi's end-of-call-report writes back a structured summary Mnemix can recall next time. Outbound calls work differently — there's no pre-call webhook for them, so your own backend calls Mnemix before dialing and passes the result in as assistantOverrides.

What
Mnemix is the memory + real-world enrichment layer for AI agents — voice-first for phone callers.
Who
For developers building AI voice agents on Vapi, Retell, or Bland.
Price
Hobby is $0. Starter, Pro, and Elite tiers — contact sales for pricing.
How
For cold voice callers, call POST /v1/recall_and_enrich before the first turn; it creates the contact on miss and starts Trestle and Twilio Lookup enrichment. Use POST /v1/calls/end for post-call write-back and GET /v1/caller/{phone_number} for read-only caller profiles. Designed for sub-300ms voice recall at the Cloudflare edge.

What Mnemix adds to Vapi

Code sample

SDK form with @mnemix-ai/client.

import { Mnemix } from "@mnemix-ai/client";

const mx = new Mnemix({ apiKey: process.env.MNEMIX_API_KEY! });

// Vapi sends EVERY event to one Server URL and tags each POST with
// message.type — there is no separate "pre-call" vs "post-call" webhook URL
// like some platforms use. Set this as your account-level Server URL
// (Dashboard > Settings) or per-assistant `serverUrl` field.
// Full event list: https://docs.vapi.ai/server-url/events
export async function POST(req: Request) {
  const { message } = await req.json();

  switch (message.type) {
    // Fires BEFORE the assistant is attached to an INBOUND call, when the
    // called phone number has no assistantId preconfigured. This is Vapi's
    // real pre-call hook — not a mid-call tool call, not dynamic-variables
    // set at dial time. Vapi blocks call setup on your response and the docs
    // say to respond within ~7.5s (telephony enforces a ~15s cap end-to-end
    // and Vapi reserves the rest for call setup), so recall_and_enrich must
    // stay fast here.
    // Docs: https://docs.vapi.ai/server-url/events, https://docs.vapi.ai/phone-calling
    case "assistant-request": {
      // Verified field name: Vapi's own Create Call request body uses
      // `customer.number` (E.164) for the caller, and assistant-request
      // embeds the same Call Object under message.call — so this path
      // should hold. Flagging moderate confidence: Vapi's docs did not show
      // us a literal end-to-end JSON dump of the assistant-request payload
      // with this field populated, only the Call Object field list and the
      // Create Call request shape separately. Log message.call once in
      // staging to confirm before relying on it in production.
      const phoneNumber = message.call?.customer?.number;
      if (!phoneNumber) {
        return Response.json({}); // let Vapi fall back to its default assistant
      }

      const { caller, memory, enrichment } = await mx.recall_and_enrich({
        phone_number: phoneNumber,
        trigger: "ringing",
        session_id: message.call?.id,
      });

      // assistantOverrides.variableValues is Vapi's documented mechanism for
      // filling {{double-curly}} placeholders in the assistant's prompt /
      // first message. Values are used live for this call only — Vapi does
      // not persist them. https://docs.vapi.ai/assistants/dynamic-variables
      return Response.json({
        assistantId: process.env.VAPI_ASSISTANT_ID,
        assistantOverrides: {
          variableValues: {
            caller_name: caller?.name ?? "there",
            caller_summary: memory?.summary ?? "",
            caller_company: enrichment?.company ?? "",
          },
        },
      });
    }

    // Fires once, after the call ends, carrying the transcript + recording.
    // https://docs.vapi.ai/server-url/events
    case "end-of-call-report": {
      const { call, artifact, endedReason } = message;

      // Vapi's artifact.messages entries use {role, message}; Mnemix expects
      // {role, text, ts_ms}. The docs we verified do not expose a per-turn
      // wall-clock timestamp on artifact.messages, so ts_ms below is a
      // best-effort ordering value (call start + turn index), not a
      // measured latency — do not treat it as authoritative timing.
      const startedAtMs = call?.startedAt ? new Date(call.startedAt).getTime() : Date.now();
      const transcript = (artifact?.messages ?? [])
        .filter((m: { role: string }) => m.role === "user" || m.role === "assistant")
        .map((m: { role: string; message: string }, i: number) => ({
          role: m.role === "assistant" ? ("agent" as const) : ("user" as const),
          text: m.message,
          ts_ms: startedAtMs + i * 1000,
        }));

      const durationS =
        call?.startedAt && call?.endedAt
          ? Math.round((new Date(call.endedAt).getTime() - new Date(call.startedAt).getTime()) / 1000)
          : 0;

      await mx.calls_end({
        session_id: call?.id,
        phone_number: call?.customer?.number,
        transcript,
        duration_s: durationS,
        outcome: endedReason ?? "other",
        agent_metadata: { platform: "vapi", assistant_id: call?.assistantId },
      });

      return Response.json({ received: true });
    }

    // OUTBOUND calls never trigger assistant-request — you already control
    // the /call request body. Call recall_and_enrich() in your own backend
    // BEFORE you POST to Vapi's /call endpoint, then pass the result as
    // assistantOverrides.variableValues in that same request:
    //
    //   const { caller, memory } = await mx.recall_and_enrich({
    //     phone_number: destinationNumber,
    //     trigger: "ringing",
    //   });
    //   await fetch("https://api.vapi.ai/call", {
    //     method: "POST",
    //     headers: { Authorization: \`Bearer \${VAPI_API_KEY}\`, "Content-Type": "application/json" },
    //     body: JSON.stringify({
    //       assistantId: VAPI_ASSISTANT_ID,
    //       phoneNumberId: VAPI_PHONE_NUMBER_ID,
    //       customer: { number: destinationNumber },
    //       assistantOverrides: { variableValues: { caller_name: caller?.name, caller_summary: memory?.summary } },
    //     }),
    //   });
    //
    // Source: https://docs.vapi.ai/calls/outbound-calling

    default:
      return Response.json({ received: true });
  }
}

Works today — call the REST API directly:

// Same handler, no SDK — raw fetch() calls to Mnemix's REST API.
// Deploy this as your Vapi Server URL (account-level or per-assistant).

export async function POST(req: Request) {
  const { message } = await req.json();

  if (message.type === "assistant-request") {
    const phoneNumber = message.call?.customer?.number;
    if (!phoneNumber) return Response.json({});

    const mxResp = await fetch("https://mcp.mnemix.ai/v1/recall_and_enrich", {
      method: "POST",
      headers: {
        Authorization: `Bearer ${process.env.MNEMIX_API_KEY}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        phone_number: phoneNumber,
        trigger: "ringing",
        session_id: message.call?.id,
      }),
    });
    const { caller, memory, enrichment } = await mxResp.json();

    return Response.json({
      assistantId: process.env.VAPI_ASSISTANT_ID,
      assistantOverrides: {
        variableValues: {
          caller_name: caller?.name ?? "there",
          caller_summary: memory?.summary ?? "",
          caller_company: enrichment?.company ?? "",
        },
      },
    });
  }

  if (message.type === "end-of-call-report") {
    const { call, artifact, endedReason } = message;
    const startedAtMs = call?.startedAt ? new Date(call.startedAt).getTime() : Date.now();

    // artifact.messages is {role, message} per Vapi; Mnemix wants
    // {role, text, ts_ms} — ts_ms here is ordering only, not measured timing.
    const transcript = (artifact?.messages ?? [])
      .filter((m) => m.role === "user" || m.role === "assistant")
      .map((m, i) => ({
        role: m.role === "assistant" ? "agent" : "user",
        text: m.message,
        ts_ms: startedAtMs + i * 1000,
      }));

    const durationS =
      call?.startedAt && call?.endedAt
        ? Math.round((new Date(call.endedAt).getTime() - new Date(call.startedAt).getTime()) / 1000)
        : 0;

    await fetch("https://mcp.mnemix.ai/v1/calls/end", {
      method: "POST",
      headers: {
        Authorization: `Bearer ${process.env.MNEMIX_API_KEY}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        session_id: call?.id,
        phone_number: call?.customer?.number,
        transcript,
        duration_s: durationS,
        outcome: endedReason ?? "other",
        agent_metadata: { platform: "vapi", assistant_id: call?.assistantId },
      }),
    });

    return Response.json({ received: true });
  }

  return Response.json({ received: true });
}

// Outbound: no assistant-request fires. Call recall_and_enrich yourself
// before POSTing https://api.vapi.ai/call, then pass the result through
// assistantOverrides.variableValues in that same request body.

FAQ

How long does the Vapi integration actually take?
Budget about 12 minutes, not the ~6 you'd spend on a platform with separate pre-call/post-call webhook URLs. Vapi funnels every event through one Server URL tagged by message.type, so you're writing a small router (assistant-request vs end-of-call-report) rather than two flat handlers, and you need to account for the inbound/outbound asymmetry below.
Does this cover inbound and outbound calls the same way?
No — they're wired differently. Inbound calls get automatic pre-call injection: Vapi's assistant-request message blocks call setup and asks your server for the assistant + variables, which is where recall_and_enrich runs. Outbound calls skip that webhook entirely because you already control the /call request; your backend calls recall_and_enrich before dialing and passes the result in via assistantOverrides on the same request.
What happens if Mnemix enrichment misses or times out?
Vapi enforces roughly a 7.5-second response budget on assistant-request (telephony caps the whole handshake at ~15s). If recall_and_enrich doesn't return in time or comes back empty, respond with just an assistantId and no variableValues — the call proceeds with the assistant's default prompt instead of stalling.
Which enrichment vendors are supported?
Mnemix's public enrichment providers are Trestle and Twilio Lookup.
What happens to calls if Mnemix goes down?
Non-blocking by design. A failed or slow recall_and_enrich call during assistant-request just means you return without variableValues, so the assistant still answers with its default configuration. Failed calls/end write-backs retry on a queue rather than blocking or dropping the call.

Last updated: .