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
- Caller identity resolved before the assistant picks up On inbound calls, Vapi's assistant-request message pauses call setup and asks your server which assistant (and which variables) to use. Mnemix answers that request with Twilio Lookup + Trestle enrichment and prior memory already folded into assistantOverrides.variableValues, inside Vapi's ~7.5s response budget.
- Structured post-call write-back on every completed call Vapi's end-of-call-report carries the full transcript and call metadata in one shot. Mnemix maps it into a session record so the next call — inbound or outbound — starts with what actually happened, not a blank slate.
- Same memory layer covers both call directions, asymmetrically wired Inbound gets automatic pre-call injection via the assistant-request hook; outbound has no equivalent webhook, so your own backend calls Mnemix before placing the Vapi /call request and forwards the result the same way. One memory store, two integration points.
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: .