Skip to main content

WhatsApp Connector

Connect LiveKit to a WhatsApp Business phone number for voice calls.

Available in
Beta

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:

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:

  1. Your app calls the DialWhatsAppCall API 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.
  2. When the call is ready to connect, Meta sends a call connect webhook  containing the SDP answer.

  3. Your app calls the ConnectWhatsAppCall API with the WhatsAppCallId and 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:

  1. Use the DialWhatsAppCall API to initiate an outbound call with the following parameters:

    ParameterRequiredDescription
    WhatsAppPhoneNumberIdYesYour WhatsApp Business phone number  ID.
    WhatsAppToPhoneNumberYesThe user's WhatsApp number to call. Must include the country code without the leading + sign.
    WhatsAppApiKeyYesYour WhatsApp API access token. To learn more, see Generate an access token .
    WhatsAppCloudApiVersionYesThe WhatsApp Cloud API version (for example, 23.0 or 24.0).
    DestinationCountryNoOptional two letter country code for the country where the call terminates. See Regional routing.

    Other optional fields include: Agents, ParticipantMetadata, ParticipantAttributes, and RingingTimeout.

    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', // optional
    roomName: 'whatsapp-connector-test', // optional
    participantIdentity: 'test-identity', // optional
    participantName: 'test-user', // optional
    agents: [{ agentName: 'my-agent' }],
    });
    from livekit import api
    lkapi = api.LiveKitAPI()
    from livekit.protocol.agent_dispatch import RoomAgentDispatch
    res = 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", # optional
    room_name="whatsapp-connector-test", # optional
    participant_identity="test-identity", # optional
    participant_name="test-user", # optional
    agents=[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", // optional
    RoomName: "whatsapp-connector-test", // optional
    ParticipantIdentity: "test-identity", // optional
    ParticipantName: "test-user", // optional
    Agents: []*livekit.RoomAgentDispatch{
    {
    AgentName: "my-agent",
    },
    },
    })
    import io.livekit.server.ConnectorServiceClient
    import io.livekit.server.WhatsAppCallOptions
    val 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.RoomAgentDispatch
    val 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", // optional
    roomName = "whatsapp-connector-test", // optional
    participantIdentity = "test-identity", // optional
    participantName = "test-user", // optional
    agents = 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()), // optional
    room_name: Some("whatsapp-connector-test".into()), // optional
    participant_identity: Some("test-identity".into()), // optional
    participant_name: Some("test-user".into()), // optional
    agents: Some(vec![RoomAgentDispatch {
    agent_name: "my-agent".into(),
    ..Default::default()
    }]),
    ..Default::default()
    },
    )
    .await?;

    The response includes a WhatsAppCallId from Meta and a RoomName (either provided using the RoomName parameter or auto-generated).

  2. Meta then sends a call connect webhook  containing the SDP answer.

    Upon receiving the webhook, call ConnectWhatsAppCall immediately with the WhatsAppCallId and 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 SessionDescription
    res = 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.SessionDescription
    val 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 silence

    Because the user's phone starts ringing when the DialWhatsAppCall call is processed, delays in calling ConnectWhatsAppCall after 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.

Automatic cleanup after 30 seconds

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

ParameterRequiredDescription
whatsapp_call_idYesThe call ID provided by Meta.
whatsapp_api_keyConditionalYour Meta API key. Required when disconnect_reason is BUSINESS_INITIATED. Optional for USER_INITIATED because no API call to WhatsApp is needed.
disconnect_reasonNoThe 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. Requires whatsapp_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.DisconnectReason
val 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.DisconnectReason
val 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

  1. A user calls your WhatsApp Business number.

  2. Meta sends a call connect webhook containing call details and the SDP offer.

  3. Your app calls AcceptWhatsAppCall with the information from the webhook to accept the call.

  4. The API returns the RoomName for 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:

ParameterRequiredDescription
WhatsAppPhoneNumberIdYesYour WhatsApp Business phone number  ID.
WhatsAppApiKeyYesYour Meta API key. To learn more, see Generate an access token .
WhatsAppCloudApiVersionYesWhatsApp Cloud API  version (for example, 23.0 or 24.0).
WhatsAppCallIdYesWhatsApp call ID provided by Meta in the webhook.
SdpYesThe 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 handler
async 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 handler
async 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 handler
func handleWhatsAppCallWebhook(w http.ResponseWriter, r *http.Request) {
// Parse webhook payload from Meta
var webhookData WhatsAppCallWebhook
json.NewDecoder(r.Body).Decode(&webhookData)
// Accept the call
response, 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 error
w.WriteHeader(http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusOK)
}
import io.livekit.server.WhatsAppCallOptions
import livekit.LivekitAgentDispatch.RoomAgentDispatch
import livekit.LivekitRtc.SessionDescription
// In your webhook handler
fun 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 handler
async 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:

  1. Sign in to the Meta Developer Console  and select your app.
  2. Select WhatsAppConfiguration.
  3. Enter the webhook URL in Callback URL.
  4. Enter any string for Verify token. Save it for your webhook handler.
  5. Subscribe to the events you want to receive. At a minimum, enable the calls event.
  6. 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 parameters
agents: [
{
agentName: 'whatsapp-agent',
metadata: '{"language": "en", "department": "sales"}',
},
],
from livekit.protocol.agent_dispatch import RoomAgentDispatch
# ... other parameters
agents=[
RoomAgentDispatch(
agent_name="whatsapp-agent",
metadata='{"language": "en", "department": "sales"}',
),
],
// ... other parameters
Agents: []*livekit.RoomAgentDispatch{
{
AgentName: "whatsapp-agent",
Metadata: `{"language": "en", "department": "sales"}`,
},
},
import livekit.LivekitAgentDispatch.RoomAgentDispatch
// ... other parameters
agents = listOf(
RoomAgentDispatch.newBuilder()
.setAgentName("whatsapp-agent")
.setMetadata("""{"language": "en", "department": "sales"}""")
.build(),
),
use livekit_protocol::RoomAgentDispatch;
// ... other parameters
agents: 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 parameters
destinationCountry: 'US',
});
response = await lkapi.connector.dial_whatsapp_call(
api.DialWhatsAppCallRequest(
# ... other parameters
destination_country="US",
)
)
response, err := connectorClient.DialWhatsAppCall(context.Background(), &livekit.DialWhatsAppCallRequest{
// ... other parameters
DestinationCountry: "US",
})
val response = connectorClient.dialWhatsAppCall(
// ... other required parameters
options = WhatsAppCallOptions(
destinationCountry = "US",
),
).execute()
let response = connector_client
.dial_whatsapp_call(
// ... other required parameters
DialWhatsAppCallOptions {
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_country to optimize routing.

Webhook not receiving events

  • Verify the webhook URL.
  • Ensure the endpoint is publicly accessible.
  • Check event subscriptions.
  • Validate webhook signatures if enabled.

Next steps