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, 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 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.
Which enrichment vendors are supported?
Mnemix's public enrichment providers are Trestle and Twilio Lookup.
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: .