Voice agent integration · 15 minutes quickstart · Inbound Call Webhook (webhook_url)

Mnemix + Retell AI

Retell AI's core is a Custom LLM WebSocket, but you don't need to take over that socket to add memory. Mnemix hooks into Retell's lighter "Inbound Call Webhook" (the `call_inbound` event) to resolve the caller before the call connects, returning recall_and_enrich results as `retell_llm_dynamic_variables` the agent's prompt can reference by `{{name}}`. When the call ends, Retell's `call_analyzed` webhook fires back the structured transcript and analysis, which Mnemix writes to the caller's profile for next-call recall — for both inbound and outbound calls.

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, Bland, LiveKit, or Twilio.
Price
Hobby $0 (free tier). Starter, Pro, and Elite tiers — contact sales for pricing while billing is in private beta.
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 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 Retell AI

Code sample

SDK form with @mnemix-ai/client.

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

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

// Retell POSTs both event types to the same webhook_url — branch on
// payload.event, the same one-URL pattern most platforms here use.
// Set this URL as the agent's `webhook_url` (per-agent) or in the
// account-level Webhooks tab of the Retell dashboard.
export async function POST(req: Request) {
  const payload = await req.json();

  // ── PRE-CALL: Retell's "Inbound Call Webhook" ────────────────────────────
  // Fires BEFORE the call is answered. Retell's own docs note there is no
  // call_id yet at this point ("you will not have a call / SMS object and
  // call / SMS id inside the payload"), so from_number is the only stable
  // key. This is the lighter path — no Custom LLM WebSocket takeover needed.
  if (payload.event === "call_inbound") {
    const { from_number } = payload.call_inbound;

    const { caller, memory, enrichment } = await mx.recall_and_enrich({
      phone_number: from_number,
      trigger: "ringing",
    });

    // Verified constraint (docs.retellai.com/build/dynamic-variables):
    // every value under dynamic_variables MUST be a string — numbers/booleans
    // are rejected. Cast everything explicitly.
    return Response.json({
      call_inbound: {
        dynamic_variables: {
          caller_name: caller?.name ?? "there",
          caller_summary: memory?.summary ?? "",
          caller_company: enrichment?.company ?? "",
          is_returning_caller: String(Boolean(caller)),
        },
      },
    });
  }

  // ── POST-CALL: Retell's call_analyzed webhook ────────────────────────────
  // Fires call_started -> call_ended -> call_analyzed per call, for both
  // inbound and outbound (payload shape is identical; only `direction`
  // differs). Retell also documents an `x-retell-signature` header for
  // authenticity — verify it in production (omitted here for brevity; use
  // Retell's SDK verify() helper).
  if (payload.event === "call_analyzed") {
    const { call } = payload;

    await mx.calls_end({
      session_id: call.call_id,
      phone_number: call.direction === "inbound" ? call.from_number : call.to_number,
      // Verified shape (docs.retellai.com/api-references/get-call):
      // transcript_object[].role is "agent" | "user" | "transfer_target",
      // content is the turn text, words[] holds per-word {word, start, end}
      // timestamps in seconds — Retell does not expose a single per-turn
      // timestamp field, so we derive one from the turn's first word.
      transcript: (call.transcript_object ?? []).map((turn: { role: string; content: string; words?: { start: number }[] }) => ({
        role: turn.role === "user" ? "user" : "agent", // "transfer_target" folded into "agent"
        text: turn.content,
        ts_ms: turn.words?.[0]?.start ? Math.round(turn.words[0].start * 1000) : 0,
      })),
      duration_s: Math.round((call.duration_ms ?? 0) / 1000),
      // Retell's call_analysis.call_successful is a boolean success signal,
      // not a task outcome — Mnemix's outcome enum is task-shaped
      // (appointment_booked / quote_given / callback_requested / no_answer),
      // so map it into agent_metadata instead of forcing it into outcome.
      outcome: "other",
      agent_metadata: {
        retell_agent_id: call.agent_id,
        call_successful: call.call_analysis?.call_successful,
        user_sentiment: call.call_analysis?.user_sentiment,
        disconnection_reason: call.disconnection_reason,
      },
    });

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

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

Works today — call the REST API directly:

# 1) Retell fires its Inbound Call Webhook BEFORE the call connects.
#    (event: "call_inbound" — verified shape, docs.retellai.com/features/inbound-call-webhook)
curl -X POST https://your-app.com/webhooks/retell/inbound \
  -H "Content-Type: application/json" \
  -d '{
    "event": "call_inbound",
    "event_timestamp": 1780012672105,
    "call_inbound": {
      "agent_id": "agent_xxx",
      "from_number": "+15551234567",
      "to_number": "+15557654321"
    }
  }'

# Your handler calls Mnemix directly:
curl -X POST https://mcp.mnemix.ai/v1/recall_and_enrich \
  -H "Authorization: Bearer $MNEMIX_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"phone_number": "+15551234567", "trigger": "ringing"}'

# ...and responds to Retell (all dynamic_variables values MUST be strings):
# {
#   "call_inbound": {
#     "dynamic_variables": { "caller_name": "Jane Doe", "is_returning_caller": "true" }
#   }
# }

# 2) Retell fires call_analyzed after the call ends (same shape for inbound
#    and outbound; direction field differs). Your handler writes back:
curl -X POST https://mcp.mnemix.ai/v1/calls/end \
  -H "Authorization: Bearer $MNEMIX_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "session_id": "call_xxx",
    "phone_number": "+15551234567",
    "transcript": [
      {"role": "agent", "text": "Hi, thanks for calling.", "ts_ms": 0},
      {"role": "user", "text": "Hey, I called last week about my order.", "ts_ms": 3200}
    ],
    "duration_s": 142,
    "outcome": "other"
  }'

FAQ

How long does the Retell integration take to wire up?
About 15-20 minutes if you already have a deployed HTTP endpoint. It's two small handlers — no Custom LLM WebSocket takeover required — plus setting webhook_url in the Retell dashboard or agent config.
Does this cover inbound and outbound calls?
Yes. Retell's webhook payload shape is identical for both; only the direction field differs ("inbound" vs "outbound"). For outbound calls placed via Retell's Create Phone Call API, you can also call recall_and_enrich yourself before placing the call and pass the result straight into retell_llm_dynamic_variables on that API call, skipping the inbound webhook path entirely.
What happens if enrichment misses — no record for this caller?
recall_and_enrich returns an empty caller/memory gracefully rather than erroring. Your handler should fall back to safe default strings (e.g. caller_name: "there") so the call_inbound response is always valid and the agent proceeds normally.
Can I bring my own enrichment vendor instead of Mnemix's bundled ones?
Trestle and Twilio Lookup are the bundled providers in Wave 1. BYO vendors are supported on paid plans — contact hello@mnemix.ai to configure a custom enrichment source.
What happens if Mnemix is down when Retell's call_inbound webhook fires?
Non-blocking by design: if recall_and_enrich times out, return call_inbound with no dynamic_variables (or a cached fallback) so the call still connects on schedule. Failed call_analyzed write-backs retry on a queue rather than blocking the webhook response.

Last updated: .