Overview
The WhatsApp Connector bridges LiveKit with the WhatsApp communication platform, providing bidirectional audio streaming and media processing—including resampling, mixing, and codec translation. It manages all API calls needed to initiate, connect, and control the call lifecycle. The connector lets you bring WhatsApp calls directly into a LiveKit room, where you can optionally dispatch LiveKit Agents to handle the interaction.
WhatsApp participants can be identified using the kind field, which identifies the type of participant in a LiveKit room. For WhatsApp participants, this is CONNECTOR.
Use cases
Use the WhatsApp Connector to build customer support workflows, triage systems, appointment and reminder flows, or outbound engagement experiences. For example, an agent can speak with a user during a call, then immediately send an invoice or follow-up information as a text message without switching channels.
Prerequisites
To use the WhatsApp Connector, you need the following:
- A phone number registered with a WhatsApp Business account .
- A WhatsApp Cloud API access token.
- A Meta Developer Account to create and manage your app.
- An app capable of receiving and handling WhatsApp webhooks .
- Call permissions enabled on the Meta platform:
- Inbound calls: Configure available call hours for your business. To learn more, see User-initiated calls .
- Outbound calls: Available only in certain regions. You must be in a supported region and obtain explicit permission from the user before initiating a call. To learn more, see Business-initiated calls .
The WhatsApp Connector works with WhatsApp Cloud API v23.0 or v24.0.
Key concepts
The WhatsApp Connector relies on webhooks and SDP negotiation to establish and manage calls.
Webhooks
Webhooks are automated, realtime notifications that one application sends to another when specific events occur. They work by delivering an HTTP POST request to a designated URL. WhatsApp uses webhooks to notify your app whenever something happens, such as an incoming call or message.
For both inbound and outbound calls, WhatsApp sends a call connect webhook that includes the Session Description Protocol (SDP) offer or answer needed to establish the connection. Your app must receive this webhook, extract the SDP, and pass it to a LiveKit Connector API to complete the connection.
Configuring a webhook endpoint is required to use the connector. Without it, your app cannot detect when a call is ready to connect or retrieve the SDP offer or answer needed to complete the setup.
To set up webhooks for your WhatsApp Business account, see the webhook configuration guide .
SDP
SDP is a standardized format used in multimedia communications to describe the parameters of a media session, such as media types, codecs, and transport protocols. WhatsApp uses SDP offer and answer negotiation to establish a media connection with LiveKit. For inbound calls, WhatsApp sends an SDP offer; for outbound calls, it sends an SDP answer.
Making outbound calls
An outbound call is a WhatsApp call initiated from a WhatsApp Business account to a user's WhatsApp number. Outbound calling is not available in all regions. Check for feature availability .
Workflow
The flow for a business-initiated (outbound) call is as follows:
Your app calls the
DialWhatsAppCallAPI to initiate an outbound call.- This API call delegates to the WhatsApp Cloud API to initiate the call and returns a
WhatsAppCallId. - Meta begins dialing the user's WhatsApp number.
- This API call delegates to the WhatsApp Cloud API to initiate the call and returns a
When the call is ready to connect, Meta sends a
call connectwebhook containing the SDP answer.Your app calls the
ConnectWhatsAppCallAPI with theWhatsAppCallIdand the SDP answer to complete the connection.
The following diagram illustrates the outbound WhatsApp call flow:
Loading diagram…
Required webhooks
For outbound calls, WhatsApp sends a call connect webhook that includes the SDP answer. Your app must receive this webhook and pass the SDP to ConnectWhatsAppCall to complete the connection.
Example
Completing an outbound call is a multi-step process:
Use the
DialWhatsAppCallAPI to initiate an outbound call with the following parameters:Parameter Required Description WhatsAppPhoneNumberIdYes Your WhatsApp Business phone number ID. WhatsAppToPhoneNumberYes The user's WhatsApp number to call. Must include the country code without the leading +sign.WhatsAppApiKeyYes Your WhatsApp API access token. To learn more, see Generate an access token . WhatsAppCloudApiVersionYes The WhatsApp Cloud API version (for example, 23.0or24.0).DestinationCountryNo Optional two letter country code for the country where the call terminates. See Regional routing. Other optional fields include:
Agents,ParticipantMetadata,ParticipantAttributes, andRingingTimeout.For a full list of parameters and their descriptions, see the DialWhatsAppCall API reference.
import { ConnectorClient } from 'livekit-server-sdk';const connectorClient = new ConnectorClient(process.env.LIVEKIT_URL,process.env.LIVEKIT_API_KEY,process.env.LIVEKIT_API_SECRET,);const res = await connectorClient.dialWhatsAppCall({whatsappPhoneNumberId: 'whatsapp-business-phone-number-id',whatsappToPhoneNumber: 'user-number-to-dial',whatsappCloudApiVersion: '23.0',whatsappApiKey: 'your-meta-access-token',destinationCountry: 'US', // optionalroomName: 'whatsapp-connector-test', // optionalparticipantIdentity: 'test-identity', // optionalparticipantName: 'test-user', // optionalagents: [{ agentName: 'my-agent' }],});from livekit import apilkapi = api.LiveKitAPI()from livekit.protocol.agent_dispatch import RoomAgentDispatchres = await lkapi.connector.dial_whatsapp_call(api.DialWhatsAppCallRequest(whatsapp_phone_number_id="whatsapp-business-phone-number-id",whatsapp_to_phone_number="user-number-to-dial",whatsapp_cloud_api_version="23.0",whatsapp_api_key="your-meta-access-token",destination_country="US", # optionalroom_name="whatsapp-connector-test", # optionalparticipant_identity="test-identity", # optionalparticipant_name="test-user", # optionalagents=[RoomAgentDispatch(agent_name="my-agent")],))res, err := connectorClient.DialWhatsAppCall(ctx, &livekit.DialWhatsAppCallRequest{WhatsappPhoneNumberId: "whatsapp-business-phone-number-id",WhatsappToPhoneNumber: "user-number-to-dial",WhatsappCloudApiVersion: "23.0",WhatsappApiKey: "your-meta-access-token",DestinationCountry: "US", // optionalRoomName: "whatsapp-connector-test", // optionalParticipantIdentity: "test-identity", // optionalParticipantName: "test-user", // optionalAgents: []*livekit.RoomAgentDispatch{{AgentName: "my-agent",},},})import io.livekit.server.ConnectorServiceClientimport io.livekit.server.WhatsAppCallOptionsval connectorClient = ConnectorServiceClient.createClient(host = System.getenv("LIVEKIT_URL").replaceFirst(Regex("^ws"), "http"),apiKey = System.getenv("LIVEKIT_API_KEY"),secret = System.getenv("LIVEKIT_API_SECRET"),)import livekit.LivekitAgentDispatch.RoomAgentDispatchval res = connectorClient.dialWhatsAppCall(whatsappPhoneNumberId = "whatsapp-business-phone-number-id",whatsappToPhoneNumber = "user-number-to-dial",whatsappApiKey = "your-meta-access-token",whatsappCloudApiVersion = "23.0",options = WhatsAppCallOptions(destinationCountry = "US", // optionalroomName = "whatsapp-connector-test", // optionalparticipantIdentity = "test-identity", // optionalparticipantName = "test-user", // optionalagents = listOf(RoomAgentDispatch.newBuilder().setAgentName("my-agent").build(),),),).execute()use livekit_api::services::connector::{ConnectorClient, DialWhatsAppCallOptions};let connector_client = ConnectorClient::with_api_key(host, api_key, api_secret);use livekit_protocol::RoomAgentDispatch;let res = connector_client.dial_whatsapp_call("whatsapp-business-phone-number-id","user-number-to-dial","your-meta-access-token","23.0",DialWhatsAppCallOptions {destination_country: Some("US".into()), // optionalroom_name: Some("whatsapp-connector-test".into()), // optionalparticipant_identity: Some("test-identity".into()), // optionalparticipant_name: Some("test-user".into()), // optionalagents: Some(vec![RoomAgentDispatch {agent_name: "my-agent".into(),..Default::default()}]),..Default::default()},).await?;The response includes a
WhatsAppCallIdfrom Meta and aRoomName(either provided using theRoomNameparameter or auto-generated).Meta then sends a
call connectwebhook containing the SDP answer.Upon receiving the webhook, call
ConnectWhatsAppCallimmediately with theWhatsAppCallIdand the SDP answer from the webhook to complete the connection:const res = await connectorClient.connectWhatsAppCall(call.id,{ type: 'answer', sdp: call.session.sdp },);from livekit.protocol.rtc import SessionDescriptionres = await lkapi.connector.connect_whatsapp_call(api.ConnectWhatsAppCallRequest(whatsapp_call_id=call.id,sdp=SessionDescription(type="answer", sdp=call.session.sdp),))res, err := connectorClient.ConnectWhatsAppCall(ctx, &livekit.ConnectWhatsAppCallRequest{WhatsappCallId: call.ID,Sdp: &livekit.SessionDescription{Type: "answer",Sdp: call.Session.SDP,},})import livekit.LivekitRtc.SessionDescriptionval res = connectorClient.connectWhatsAppCall(whatsappCallId = call.id,sdp = SessionDescription.newBuilder().setType("answer").setSdp(call.session.sdp).build(),).execute()use livekit_protocol::SessionDescription;let res = connector_client.connect_whatsapp_call(&call.id,SessionDescription {r#type: "answer".into(),sdp: call.session.sdp.clone(),},).await?;Delayed connection can result in silenceBecause the user's phone starts ringing when the
DialWhatsAppCallcall is processed, delays in callingConnectWhatsAppCallafter the webhook is received can result in silence and eventual disconnection.
Disconnecting calls
Use DisconnectWhatsAppCall to end an active WhatsApp call. You must call this API for both business-initiated and user-initiated disconnects. When a user hangs up, Meta sends a call terminate webhook to your app. Your webhook handler must then call DisconnectWhatsAppCall with USER_INITIATED so LiveKit can clean up the connector session and room resources.
If you don't call DisconnectWhatsAppCall after a user hangs up, LiveKit automatically cleans up the call after 30 seconds. During this window, any agents, egress, or other services running in the room continue to run unnecessarily. Always call the API promptly to avoid wasted resources.
Parameters
| Parameter | Required | Description |
|---|---|---|
whatsapp_call_id | Yes | The call ID provided by Meta. |
whatsapp_api_key | Conditional | Your Meta API key. Required when disconnect_reason is BUSINESS_INITIATED. Optional for USER_INITIATED because no API call to WhatsApp is needed. |
disconnect_reason | No | The reason for disconnecting the call. Defaults to BUSINESS_INITIATED. |
The disconnect_reason field accepts one of the following values:
BUSINESS_INITIATED: The business is ending the call. Requireswhatsapp_api_key.USER_INITIATED: The user ended the call. Use this when you receive a call terminate webhook from Meta. Note that Meta also sends this webhook when the business disconnects, so calling the API twice results in an error.
Business-initiated disconnect example
await connectorClient.disconnectWhatsAppCall('call-id-from-meta','your-meta-access-token','BUSINESS_INITIATED',);
await lkapi.connector.disconnect_whatsapp_call(api.DisconnectWhatsAppCallRequest(whatsapp_call_id="call-id-from-meta",whatsapp_api_key="your-meta-access-token",disconnect_reason=api.DisconnectWhatsAppCallRequest.BUSINESS_INITIATED,))
_, err := connectorClient.DisconnectWhatsAppCall(context.Background(), &livekit.DisconnectWhatsAppCallRequest{WhatsappCallId: "call-id-from-meta",WhatsappApiKey: "your-meta-access-token",DisconnectReason: livekit.DisconnectWhatsAppCallRequest_BUSINESS_INITIATED,})if err != nil {// Handle error}
import livekit.LivekitConnectorWhatsapp.DisconnectWhatsAppCallRequest.DisconnectReasonval response = connectorClient.disconnectWhatsAppCall(whatsappCallId = "call-id-from-meta",whatsappApiKey = "your-meta-access-token",disconnectReason = DisconnectReason.BUSINESS_INITIATED,).execute()
connector_client.disconnect_whatsapp_call_with_reason("call-id-from-meta","your-meta-access-token",DisconnectReason::BusinessInitiated,).await?;
User-initiated disconnect example
When you receive a call terminate webhook from Meta indicating the user hung up, call DisconnectWhatsAppCall with USER_INITIATED to clean up the connector session. No API key is needed because no call to WhatsApp is made:
await connectorClient.disconnectWhatsAppCall('call-id-from-meta',undefined, // no API key needed'USER_INITIATED',);
await lkapi.connector.disconnect_whatsapp_call(api.DisconnectWhatsAppCallRequest(whatsapp_call_id="call-id-from-meta",disconnect_reason=api.DisconnectWhatsAppCallRequest.USER_INITIATED,))
_, err := connectorClient.DisconnectWhatsAppCall(context.Background(), &livekit.DisconnectWhatsAppCallRequest{WhatsappCallId: "call-id-from-meta",DisconnectReason: livekit.DisconnectWhatsAppCallRequest_USER_INITIATED,})
import livekit.LivekitConnectorWhatsapp.DisconnectWhatsAppCallRequest.DisconnectReasonval response = connectorClient.disconnectWhatsAppCall(whatsappCallId = "call-id-from-meta",disconnectReason = DisconnectReason.USER_INITIATED,).execute()
connector_client.disconnect_whatsapp_call_with_reason("call-id-from-meta","",DisconnectReason::UserInitiated,).await?;
Accepting inbound calls
To accept inbound WhatsApp calls, you must handle webhooks from Meta and call the AcceptWhatsAppCall API.
Workflow
A user calls your WhatsApp Business number.
Meta sends a
call connectwebhook containing call details and the SDP offer.Your app calls
AcceptWhatsAppCallwith the information from the webhook to accept the call.The API returns the
RoomNamefor the call. If the API doesn't return an error, the call is connected.
Required webhooks
For inbound calls, WhatsApp sends a call connect webhook that includes the SDP offer. Your app must receive this webhook and pass the SDP offer to the AcceptWhatsAppCall API to complete the connection.
Example
The following webhook handler example processes the call connect webhook and calls the AcceptWhatsAppCall API with the following parameters:
| Parameter | Required | Description |
|---|---|---|
WhatsAppPhoneNumberId | Yes | Your WhatsApp Business phone number ID. |
WhatsAppApiKey | Yes | Your Meta API key. To learn more, see Generate an access token . |
WhatsAppCloudApiVersion | Yes | WhatsApp Cloud API version (for example, 23.0 or 24.0). |
WhatsAppCallId | Yes | WhatsApp call ID provided by Meta in the webhook. |
Sdp | Yes | The SDP offer provided by Meta in the webhook. |
The webhook handler creates a room named whatsapp-connector-room and dispatches the agent named whatsapp-agent to the room after the connection is established:
// In your webhook handlerasync function handleWhatsAppCallWebhook(webhookData: WhatsAppCallWebhook) {const response = await connectorClient.acceptWhatsAppCall({whatsappPhoneNumberId: '<whatsapp-business-phone-number-id>',whatsappApiKey: '<meta-access-token>',whatsappCloudApiVersion: '23.0',whatsappCallId: webhookData.callId,sdp: webhookData.sdp,roomName: 'whatsapp-connector-room',agents: [{ agentName: 'whatsapp-agent' }],});}
from livekit.protocol.agent_dispatch import RoomAgentDispatch# In your webhook handlerasync def handle_whatsapp_call_webhook(webhook_data):response = await lkapi.connector.accept_whatsapp_call(api.AcceptWhatsAppCallRequest(whatsapp_phone_number_id="<whatsapp-business-phone-number-id>",whatsapp_api_key="<meta-access-token>",whatsapp_cloud_api_version="23.0",whatsapp_call_id=webhook_data.call_id,sdp=webhook_data.sdp,room_name="whatsapp-connector-room",agents=[RoomAgentDispatch(agent_name="whatsapp-agent")],))
// In your webhook handlerfunc handleWhatsAppCallWebhook(w http.ResponseWriter, r *http.Request) {// Parse webhook payload from Metavar webhookData WhatsAppCallWebhookjson.NewDecoder(r.Body).Decode(&webhookData)// Accept the callresponse, err := connectorClient.AcceptWhatsAppCall(context.Background(), &livekit.AcceptWhatsAppCallRequest{WhatsappPhoneNumberId: "<whatsapp-business-phone-number-id>",WhatsappApiKey: "<meta-access-token>",WhatsappCloudApiVersion: "23.0",WhatsappCallId: webhookData.CallId,Sdp: webhookData.Sdp,RoomName: "whatsapp-connector-room",Agents: []*livekit.RoomAgentDispatch{{AgentName: "whatsapp-agent",},},})if err != nil {// Handle errorw.WriteHeader(http.StatusInternalServerError)return}w.WriteHeader(http.StatusOK)}
import io.livekit.server.WhatsAppCallOptionsimport livekit.LivekitAgentDispatch.RoomAgentDispatchimport livekit.LivekitRtc.SessionDescription// In your webhook handlerfun handleWhatsAppCallWebhook(webhookData: WhatsAppCallWebhook) {val response = connectorClient.acceptWhatsAppCall(whatsappPhoneNumberId = "<whatsapp-business-phone-number-id>",whatsappApiKey = "<meta-access-token>",whatsappCloudApiVersion = "23.0",whatsappCallId = webhookData.callId,sdp = webhookData.sdp,options = WhatsAppCallOptions(roomName = "whatsapp-connector-room",agents = listOf(RoomAgentDispatch.newBuilder().setAgentName("whatsapp-agent").build(),),),).execute()}
use livekit_api::services::connector::{ConnectorClient, AcceptWhatsAppCallOptions};use livekit_protocol::RoomAgentDispatch;// In your webhook handlerasync fn handle_whatsapp_call_webhook(connector_client: &ConnectorClient,webhook_data: &WhatsAppCallWebhook,) -> Result<(), Box<dyn std::error::Error>> {let response = connector_client.accept_whatsapp_call("<whatsapp-business-phone-number-id>","<meta-access-token>","23.0",&webhook_data.call_id,webhook_data.sdp.clone(),AcceptWhatsAppCallOptions {room_name: Some("whatsapp-connector-room".into()),agents: Some(vec![RoomAgentDispatch {agent_name: "whatsapp-agent".into(),..Default::default()}]),..Default::default()},).await?;Ok(())}
The same optional parameters as for outbound calls are available for customizing the participant and room. For explicit agent dispatch, make sure to include the Agents parameter. Use WaitUntilAnswered to block until the call is answered before receiving a response.
For a full list of parameters and their descriptions, see the AcceptWhatsAppCall API reference.
Setting up webhooks
This section covers the required configuration steps in the Meta Developer Console. To learn more about implementing a webhook handler, see the WhatsApp webhook configuration guide .
To configure WhatsApp call webhooks:
- Sign in to the Meta Developer Console and select your app.
- Select WhatsApp → Configuration.
- Enter the webhook URL in Callback URL.
- Enter any string for Verify token. Save it for your webhook handler.
- Subscribe to the events you want to receive. At a minimum, enable the
callsevent. - Select the same Version for all subscribed webhooks: v23.0 or v24.0.
Agent dispatch
You can automatically dispatch LiveKit Agents to WhatsApp calls by including agent dispatch rules in your call requests, enabling your AI agents to interact with WhatsApp callers.
To explicitly dispatch a specific agent to an inbound or outbound call, use the Agents parameter in the AcceptWhatsAppCall or DialWhatsAppCall API:
// ... other parametersagents: [{agentName: 'whatsapp-agent',metadata: '{"language": "en", "department": "sales"}',},],
from livekit.protocol.agent_dispatch import RoomAgentDispatch# ... other parametersagents=[RoomAgentDispatch(agent_name="whatsapp-agent",metadata='{"language": "en", "department": "sales"}',),],
// ... other parametersAgents: []*livekit.RoomAgentDispatch{{AgentName: "whatsapp-agent",Metadata: `{"language": "en", "department": "sales"}`,},},
import livekit.LivekitAgentDispatch.RoomAgentDispatch// ... other parametersagents = listOf(RoomAgentDispatch.newBuilder().setAgentName("whatsapp-agent").setMetadata("""{"language": "en", "department": "sales"}""").build(),),
use livekit_protocol::RoomAgentDispatch;// ... other parametersagents: Some(vec![RoomAgentDispatch {agent_name: "whatsapp-agent".into(),metadata: r#"{"language": "en", "department": "sales"}"#.into(),}]),
For more information on creating voice agents, see the LiveKit Agents documentation.
Regional routing
Use the destination_country parameter to optimize call routing based on the caller's location. Provide an ISO 3166-1 alpha-2 code (for example, US, GB, IN).
const response = await connectorClient.dialWhatsAppCall({// ... other parametersdestinationCountry: 'US',});
response = await lkapi.connector.dial_whatsapp_call(api.DialWhatsAppCallRequest(# ... other parametersdestination_country="US",))
response, err := connectorClient.DialWhatsAppCall(context.Background(), &livekit.DialWhatsAppCallRequest{// ... other parametersDestinationCountry: "US",})
val response = connectorClient.dialWhatsAppCall(// ... other required parametersoptions = WhatsAppCallOptions(destinationCountry = "US",),).execute()
let response = connector_client.dial_whatsapp_call(// ... other required parametersDialWhatsAppCallOptions {destination_country: Some("US".into()),..Default::default()},).await?;
Troubleshooting
The following troubleshooting steps can help you resolve common issues with the WhatsApp Connector.
Call not connecting
- Verify your WhatsApp API key permissions.
- Ensure your phone number is registered and verified.
- Confirm the correct Cloud API version.
- Check webhook URL accessibility from Meta.
Audio quality issues
- Check network connectivity between LiveKit and Meta.
- Confirm that media tracks are being published correctly.
- Use
destination_countryto optimize routing.
Webhook not receiving events
- Verify the webhook URL.
- Ensure the endpoint is publicly accessible.
- Check event subscriptions.
- Validate webhook signatures if enabled.