Guides · 18 / 42

Retell

Point your agent's inbound-call webhook and call webhook at your worker, enable call_analyzed, call POST /v1/recall_and_enrich on call_inbound and POST /v1/calls/end on call_analyzed, and keep your key as a server-side secret.

The Northsun Retell kit is a 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. The kit is published on npm under its current scope, and the versions there are older than the behaviour described on this page, which is the kit's current source; the next release ships under the Northsun package name (in progress). Until then, the two HTTP calls documented on this page work without the kit, as shown on Voice integration patterns; the contact form can supply the current package name. This is a Northsun-built, Northsun-maintained kit against Retell's documented webhook contract — not a native Retell feature or a certified Retell integration.

Set your key

NORTHSUN_API_KEY=sk_live_...
NORTHSUN_BASE_URL=https://mcp.northsun.ai   # set it explicitly; see below
RETELL_API_KEY=...                      # the key with the "webhook" badge, recommended

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

These are names you choose. Pass the key and the base URL https://mcp.northsun.ai to the client's options explicitly: the kit reads no environment itself, and the published client defaults to an earlier hostname that serves the same surface.

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
call_inbound   -> verify x-retell-signature, POST /v1/recall_and_enrich, return the call_inbound envelope
call_analyzed  -> verify x-retell-signature, POST /v1/calls/end

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.

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 six host-safe fields (is_returning, history_known, caller_known, has_memory_facts, line_type and memory_status), 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. Reference the stringified fields in your agent's existing prompt with Retell's {{variable_name}} templating. Caller-derived text (name, company, summary, facts) is not exposed as variables: a stored value would be interpolated into your agent's prompt outside the quoted data block. It reaches the model only inside the quoted memory block (memory_block). Return response from the inbound handler and deliver the memory block separately when you make the model call: in a custom-LLM handler, send memory_block.text as a non-system message. If you configure agent_override from system_prompt yourself, append memory_block.text after every instruction to keep caller memory; that puts memory in the system prompt, so prefer the separate message when your integration allows it.

memory_status is a tri-state, because an empty memory block 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. The returned facts are 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). The facts list may be empty here too, but it 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 host-safe 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.

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. The result's signature_verified field 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 the 204 confirms and how to verify the write.

Next steps