← BlogTutorial

Add caller memory to a Vapi agent in an afternoon

Mnemix · Jun 2026 · 6 min read

This walkthrough wires Mnemix into a Vapi assistant so every inbound call starts with caller context and every completed call makes the next one smarter. It works with any Vapi setup that can hit a webhook or call a tool.

What you need

  • A Vapi assistant taking inbound calls
  • A Mnemix API key (Hobby $0)
  • Somewhere to run ~20 lines of glue (your existing server, or a serverless function)

1. Recall on ring

In your server-url handler, when Vapi signals an inbound call, recall before the assistant speaks:

const recall = 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: call.customer.number,
    trigger: "answered",
    session_id: call.id,
  }),
}).then((r) => r.json());

2. Hand the context to the assistant

Inject what came back into the assistant's system context:

const context = recall.known
  ? `Caller: ${recall.caller.name ?? "name unknown"}. ` +
    `History: ${recall.memory.summary || "no prior calls"}.`
  : "First-time caller — no history.";

Now "Hi, thanks for calling" becomes "Hi Mike — calling about Thursday's inspection?"

3. Write back on end-of-call

When Vapi sends the end-of-call report, persist it:

// Vapi's endedReason tells you *how* the call ended, not what it achieved,
// so business outcomes (appointment_booked, quote_given, callback_requested)
// come from Vapi's analysis.structuredData — configure your assistant's
// structuredDataPlan to extract an "outcome" field. endedReason still covers
// the no-answer cases. The @mnemix-ai/vapi-kit package does this mapping
// (plus fuller transcript normalization) for you if you'd rather not
// hand-roll it.
function toMnemixOutcome(report) {
  const extracted = report.analysis?.structuredData?.outcome;
  if (["appointment_booked", "quote_given", "callback_requested"].includes(extracted)) {
    return extracted;
  }
  const reason = (report.endedReason ?? "").toLowerCase();
  if (reason.includes("no-answer") || reason.includes("voicemail")) return "no_answer";
  return "other";
}

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({
    phone_number: call.customer.number,
    session_id: call.id,
    duration_s: Math.round(report.durationSeconds ?? 0),
    outcome: toMnemixOutcome(report),
    // report.messages is Vapi's structured turn list (artifact.messages);
    // Mnemix wants { role: "user" | "agent", text, ts_ms }[], not a raw string.
    transcript: (report.messages ?? [])
      .filter((m) => m.role === "user" || m.role === "bot" || m.role === "assistant")
      .map((m, i) => ({
        role: m.role === "user" ? "user" : "agent",
        text: m.message,
        ts_ms: i * 1000,
      })),
  }),
});

Mnemix stores the call as a durable interaction keyed to the caller — the next recall_and_enrich for this number returns it as history.

That's the whole integration

One call at ring, one at hang-up. The second time a customer calls, your assistant remembers them — and the loop compounds from there.