Skip to main content
By the end of this guide you’ll have a Retell agent that greets you and replies to everything you say with a fixed sentence. No LLM yet. That proves the connection works before you add a model. Connecting your LLM is the next guide. Code below is Node.js (Express) and Python (FastAPI). Any stack that serves WebSockets works, since the protocol is the same in every language.

Before you start

  • A server that can hold a WebSocket open. Serverless and edge runtimes generally can’t. Vercel edge functions and Lambda-style handlers won’t work. Use a long-running process (a container, a VM, or a persistent Node/Python server).
  • TLS in production. Use wss:// or https://. Retell treats them the same, mapping https: to wss:. Plain ws:// and http:// work too, but send transcripts unencrypted.
  • Read the protocol. The LLM WebSocket reference is the field-by-field spec. This guide covers only the fields you need to get a call working.
Install the dependencies for the code in this section, including the LLM client you’ll add in the next guide:
Retell sends no auth headers on this WebSocket. To restrict who can connect, allowlist Retell’s outbound IP address 100.20.5.228, or put a secret in the URL you configure (a path segment or query string) and reject connections that don’t carry it.
1

Add a WebSocket endpoint

Retell connects to your llm_websocket_url with the call ID appended as the final path segment. If you configure wss://your-domain.com/llm-websocket, Retell connects to wss://your-domain.com/llm-websocket/{call_id}. Capture that segment. It’s how you tie a connection to a call.
Save this as server.ts or server.py, then run it with npx tsx server.ts or uvicorn server:app --port 3000. This guide adds types.ts or custom_types.py next to it, and the next one adds llm.ts or llm.py. Keep all of them in the same directory, since the imports in each are flat.Every message is a text frame containing stringified JSON. Retell never sends binary frames, and it closes the connection with code 1007 if you send one.
2

Send config and a begin message

Your server speaks first. Send two messages as soon as the connection opens:A config event turns on optional protocol features. auto_reconnect starts the keepalive exchange so a dropped connection gets rebuilt instead of killing the call. call_details makes Retell push the whole call object to you immediately, saving you a Get Call API round trip.A response event with response_id: 0 is the begin message, the first thing the agent says. Set content to an empty string to have the agent wait for the caller to speak first.
Setting auto_reconnect: true obligates you to echo ping_pong events, which you’ll add in the next step. If Retell goes 5 seconds without one, it closes the connection and reconnects. After 2 reconnects it gives up and ends the call with error_llm_websocket_lost_connection.
If your greeting depends on call data, such as a caller’s name from a dynamic variable, send config immediately but hold the begin message until the call_details event arrives.
3

Answer each interaction type

Now handle what Retell sends. Only response_required and reminder_required need a spoken answer, and ping_pong needs an echo. update_only and call_details need no reply at all.Reply with the same response_id Retell asked with. A response carrying any other response_id is discarded silently.
Set content_complete: true on the last event of a response. Retell keeps waiting for more content until it sees that flag, so a response that never completes leaves the turn hanging: the agent stops speaking, no error is raised, and it only recovers once the caller speaks again.
4

Expose your server and point an agent at it

Retell has to reach your server over the public internet.
  • Deployed: use your own domain, wss://your-domain.com/llm-websocket
  • Local: tunnel it with ngrok (ngrok http 3000) and use the forwarding host, wss://xxxx.ngrok-free.app/llm-websocket
Create a Custom LLM agent in the dashboard, then paste the URL into the Custom LLM URL field on the agent.
Agent details panel for a Custom LLM agent, with the Custom LLM URL field outlined in blue and holding the default placeholder https://replace-with-your-llm-url.com

Replace the placeholder in Custom LLM URL with your own endpoint.

The URL supports dynamic variables, so wss://your-domain.com/llm-websocket?tenant={{tenant_id}} resolves per call. Use it to route calls to different backends, or to pass a shared secret.
5

Make a web call

Open the Test Audio tab on the agent and click Run Test. It’s the only test surface a custom LLM agent has: the Test LLM tab is hidden for them, and simulation testing rejects them.You should hear “How may I help you?”, and every time you speak the agent should answer “I am sorry, can you say that again?”. Your server logs should show a call_details event, then update_only events as you talk, then a response_required event for each reply.

Message types

Typed definitions for the events in this guide. Reuse them across the rest of the section.

FAQ

On the initial connection, Retell makes up to 3 attempts with a 7-second timeout each and 3 seconds between them. If all fail, the call ends with error_llm_websocket_open. Mid-call, Retell rebuilds a dropped connection instead of giving up: up to 2 reconnects when keepalives stop arriving, and up to 4 when the socket closes abnormally (code 1006).
No. Skip it and you get the defaults: no keepalives, no call details, no tool-call transcripts. The begin message alone is enough to start a call. Send config first if you send it at all, since Retell acts on it as it arrives.
Not on serverless or edge functions. They can’t hold a WebSocket open for the length of a call. Deploy a long-running process instead. Also watch for idle timeouts on hosts that have them: a platform that kills connections at 5 minutes will drop every call that runs longer.
Most often the response_id didn’t match what Retell asked for, or content_complete: true never arrived. Retell also discards a response outright when the caller keeps talking and asks again with a new response_id, which is expected. See handling discarded responses.
Yes. Connect to it with Postman or websocat and send a JSON frame shaped like a response_required event. Your server should answer with a response event. This separates connectivity problems from protocol problems.

Next step

Your agent answers with a fixed sentence. Connect your LLM to stream real responses.