Documentation

Retell

@mnemix-ai/retell-kit is a real, published integration kit for Retell AI: call_inbound pre-call enrichment and HMAC-verified post-call memory write-back. It calls the same two frozen routes documented on Voice integration patterns — POST /v1/recall_and_enrich before the call, POST /v1/calls/end after — and nothing else. This is a Northsun-built, Northsun-maintained kit against Retell's documented webhook contract — not a native Retell feature or a certified Retell integration.

Prefer zero package dependencies? The custom-HTTP Retell guide maps the same two calls with plain fetch and no npm install at all.

Install

npm install @mnemix-ai/retell-kit @mnemix-ai/client

Verified in a clean directory: @mnemix-ai/retell-kit@0.1.0's peer dependency on @mnemix-ai/client@^0.2.2 resolves against the published @mnemix-ai/client@0.2.2, install exits 0, and both packages import clean.

@mnemix-ai/* is today's published scope. If your integration will outlive a possible future move to @northsun-ai/*, pin the version above rather than tracking latest.

Set your key

MNEMIX_KEY=sk_live_...
MNEMIX_BASE_URL=https://mcp.mnemix.ai   # optional; this is the default
RETELL_API_KEY=...                      # the key with the "webhook" badge, recommended

MNEMIX_KEY is your Northsun API key — see Installation for how to get one. Set it as a platform secret (wrangler secret put MNEMIX_KEY on Cloudflare Workers, or your platform's equivalent), never as a plaintext environment value in a repo or client bundle.

RETELL_API_KEY is not a separate webhook-signing secret — Retell signs webhooks with the customer's own Retell API key, specifically "only the API key that has a webhook badge next to it can be used to verify the webhook." There is no other signing-secret concept on this platform.

Wire the webhooks

Retell has exactly one pre-call hook and one post-call webhook family, both pointed at your agent/workspace configuration:

Inbound-call webhook = https://your-worker.example.com/retell/call-inbound
Call webhook         = https://your-worker.example.com/retell/call-webhook
import { Mnemix } from "@mnemix-ai/client";
import { handleRetellCallInbound, handleRetellCallWebhook } from "@mnemix-ai/retell-kit";

const mnemix = new Mnemix({ apiKey: env.MNEMIX_KEY, baseUrl: env.MNEMIX_BASE_URL });
const opts = { mnemix, retellApiKey: env.RETELL_API_KEY };

const ctx = await handleRetellCallInbound(opts, payload, { rawBody, signatureHeader, tone: "warm" });
return Response.json(ctx.response); // the literal Retell call_inbound response envelope

const result = await handleRetellCallWebhook(opts, { rawBody, signatureHeader });

call_inbound fires before the call is registered — the request carries from_number/to_number but no call identifier, so the recall runs without a session_id. See packages/retell-kit/examples/cloudflare-worker.ts for the full working file, or examples/express-server.ts / examples/nextjs-route.ts for the same two routes on other runtimes.

Why this kit writes back on exactly one event, not two

Retell's documented default webhook_events for a voice agent is call_started, call_ended, call_analyzed — all delivered to the same URL — and call_analyzed "contains the full call data including call_analysis," i.e. everything call_ended has, plus the analysis block. Writing back on both would fire two /v1/calls/end requests per call under Retell's own default configuration — a wasted round-trip and doubled log lines, for zero benefit, since /v1/calls/end already dedupes by session_id.

This kit writes back on exactly one event by default: call_analyzed (the richer, superset event). call_ended is skipped by default — same bucket as call_started — unless you set writeBackOnCallEnded: true, the documented escape hatch for accounts that have customized webhook_events to exclude call_analyzed. A per-call_id dedup cache additionally guards every write-back attempt, as defense in depth against Retell's own retry policy.

Enable call_analyzed in your Retell dashboard/agent config for this kit's default posture to reach Northsun at all — call_ended and call_started are harmless to leave enabled too; this kit treats them as informational by default.

Any event this kit chooses not to write back returns { ok: true, skipped: true }, which every example maps to a 2xx response — Retell's own guidance is "always return a 2xx status as soon as you have safely accepted the payload," and non-2xx is reserved for genuinely transient failures.

What you get back

variables carries the same 13-field shape every sibling kit returns, projected into response.call_inbound.dynamic_variables with every value coerced to a string — Retell's retell_llm_dynamic_variables does not support numbers, booleans, or arrays. memory_facts is excluded from the wire payload for the same reason (arrays have no string encoding) and stays only on .variables. Reference the stringified fields directly in your agent's existing prompt with Retell's {{variable_name}} templating: {{caller_name}}, {{is_returning}}, {{last_call_summary}}, {{company}}, and so on.

memory_status is a tri-state, because memory_facts alone cannot tell "this caller genuinely has no governed facts" apart from "the facts read failed and we could not check":

  • "ok" — the read ran and answered. memory_facts is a real measurement, possibly empty.
  • "unavailable" — the read did not complete (server-side budget or error, or recall itself failed, timed out, or was never reached). memory_facts is [] here too, but that [] is not a measurement. The prompt builder renders an explicit instruction telling the agent not to state or imply the caller has no facts on file, instead of staying silent — silence would let the agent infer "nothing on file" on its own, which is the exact false statement this field exists to prevent.
  • "off" — your tenant deployment has memory-unification reads disabled. Feature off, not a failure; rendered the same as "ok" with an empty list.

Current limitation — read this before you branch on "ok". The tri-state is only as precise as what the recall API puts on the wire, and the server-side change that reports a dropped governed read distinctly is not yet released. Until it is:

  • "ok" under-warns. If the governed-facts read times out or errors for a caller who already has call history, the API still returns an empty list, and it is indistinguishable from a caller who genuinely has no stored facts. The kit reports "ok" and the prompt stays silent.
  • "unavailable" can over-warn. The kit infers it when the caller's interaction count is also unreadable, which is deliberate but is not proof the facts read itself failed. Do not diagnose a facts outage from this field alone.

The direction of both errors is deliberate: the kit would rather warn you about a record it did read than stay silent about one it did not. Treat "unavailable" as "do not assert this caller has nothing on file" rather than as a precise diagnostic, and do not treat "ok" as proof of a measurement until the server change ships. This note will be removed when it does.

The wire payload also carries memory_status_instruction, which is not one of the 13 context fields — it is the rendered instruction text for the memory_status tri-state. Retell is the one kit where ctx.system_prompt never reaches the agent (this kit deliberately does not guess an agent_override response-engine group), so add {{memory_status_instruction}} to your agent's configured prompt or the "stored facts could not be read" instruction is never delivered and your agent may assert the caller has nothing on file. It renders as the empty string whenever memory_status is "ok" or "off".

system_prompt is plain sanitized text — buildSystemPrompt() builds it — but this kit does not set it on any Retell wire field, because call_inbound's request never tells this kit which of Retell's two agent architectures (retell_llm vs conversation_flow) the agent uses, and guessing wrong risks the whole call being rejected. If you know your own agent's response-engine type, read ctx.system_prompt and set agent_override yourself.

history_known distinguishes "we checked and this caller has no history" from "we don't know" — is_returning alone can't express the second case. See What recall gives you back for the full field reference.

Webhook verification — and one path with an open question

Header: x-retell-signature: v=<unix_ms_timestamp>,d=<hex_hmac>, HMAC-SHA256(secret=retellApiKey, message=rawBody + timestamp). This was read directly out of the official retell-sdk package's shipped source and is proven bidirectionally against that SDK as a live devDependency — see packages/retell-kit/README.md's "Webhook Verification" section for the citation.

handleRetellCallWebhook() (the call-event webhook) is unambiguously documented and required when retellApiKey is set. handleRetellCallInbound()'s signature premise is not confirmed by Retell's own docs — Retell names the same verification mechanism for both, but the one page that shows a worked example destructures a different payload shape. ctx.mnemix.signature_verified is null/true/false so you can watch for this rather than have it silently read as "every caller is unknown"; verifyInboundCallSignature: false is an explicit opt-out once you've confirmed on your own traffic.

Verify it

Call the Retell number attached to your agent. Your worker logs should show one recall on call_inbound, and one POST /v1/calls/end when call_analyzed fires. See Hangup, and what happens next for what happens immediately after that call.

Next steps