From aad810c21fe19eac6276c6aa23d8a1f4ab9dfbcd Mon Sep 17 00:00:00 2001 From: Anunay Maheshwari Date: Wed, 19 Nov 2025 04:53:40 +0530 Subject: [PATCH 1/5] feat(connector): initial client impl --- packages/livekit-server-sdk/package.json | 2 +- .../livekit-server-sdk/src/ConnectorClient.ts | 283 ++++++++++++++++++ packages/livekit-server-sdk/src/index.ts | 1 + pnpm-lock.yaml | 11 +- 4 files changed, 287 insertions(+), 10 deletions(-) create mode 100644 packages/livekit-server-sdk/src/ConnectorClient.ts diff --git a/packages/livekit-server-sdk/package.json b/packages/livekit-server-sdk/package.json index 0d0f507d..7659da1e 100644 --- a/packages/livekit-server-sdk/package.json +++ b/packages/livekit-server-sdk/package.json @@ -43,7 +43,7 @@ }, "dependencies": { "@bufbuild/protobuf": "^1.10.1", - "@livekit/protocol": "^1.42.0", + "@livekit/protocol": "link:../../../protocol/packages/javascript", "camelcase-keys": "^9.0.0", "jose": "^5.1.2" }, diff --git a/packages/livekit-server-sdk/src/ConnectorClient.ts b/packages/livekit-server-sdk/src/ConnectorClient.ts new file mode 100644 index 00000000..18a16c10 --- /dev/null +++ b/packages/livekit-server-sdk/src/ConnectorClient.ts @@ -0,0 +1,283 @@ +// SPDX-FileCopyrightText: 2025 LiveKit, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +import type { RoomAgentDispatch, SessionDescription } from '@livekit/protocol'; +import { + AcceptWhatsAppCallRequest, + AcceptWhatsAppCallResponse, + ConnectTwilioCallRequest, + ConnectTwilioCallRequest_TwilioCallDirection, + ConnectTwilioCallResponse, + ConnectWhatsAppCallRequest, + ConnectWhatsAppCallResponse, + DialWhatsAppCallRequest, + DialWhatsAppCallResponse, + DisconnectWhatsAppCallRequest, + DisconnectWhatsAppCallResponse, + WhatsAppCallDirection, +} from '@livekit/protocol'; +import type { ClientOptions } from './ClientOptions.js'; +import { ServiceBase } from './ServiceBase.js'; +import { type Rpc, TwirpRpc, livekitPackage } from './TwirpRPC.js'; + +const svc = 'Connector'; + +// WhatsApp types +export interface DialWhatsAppCallOptions { + /** Required - The number of the business that is initiating the call */ + whatsappPhoneNumberId: string; + /** Required - The number of the user that is supposed to receive the call */ + whatsappToPhoneNumber: string; + /** Required - The API key of the business that is initiating the call */ + whatsappApiKey: string; + /** Required - WhatsApp Cloud API version, eg: 23.0, 24.0, etc. */ + whatsappCloudApiVersion: string; + /** Optional - An arbitrary string you can pass in that is useful for tracking and logging purposes */ + whatsappBizOpaqueCallbackData?: string; + /** Optional - What LiveKit room should this participant be connected to */ + roomName?: string; + /** Optional - Agents to dispatch the call to */ + agents?: RoomAgentDispatch[]; + /** Optional - Identity of the participant in LiveKit room */ + participantIdentity?: string; + /** Optional - Name of the participant in LiveKit room */ + participantName?: string; + /** Optional - User-defined metadata. Will be attached to a created Participant in the room. */ + participantMetadata?: string; + /** Optional - User-defined attributes. Will be attached to a created Participant in the room. */ + participantAttributes?: { [key: string]: string }; + /** Optional - Country where the call terminates as ISO 3166-1 alpha-2 */ + destinationCountry?: string; +} + +export interface AcceptWhatsAppCallOptions { + /** Required - The number of the business that is connecting the call */ + whatsappPhoneNumberId: string; + /** Required - The API key of the business that is connecting the call */ + whatsappApiKey: string; + /** Required - WhatsApp Cloud API version, eg: 23.0, 24.0, etc. */ + whatsappCloudApiVersion: string; + /** Required - Call ID sent by Meta */ + whatsappCallId: string; + /** Optional - An arbitrary string you can pass in that is useful for tracking and logging purposes */ + whatsappBizOpaqueCallbackData?: string; + /** Required - The call accept webhook comes with SDP from Meta */ + sdp: SessionDescription; + /** Optional - What LiveKit room should this participant be connected to */ + roomName?: string; + /** Optional - Agents to dispatch the call to */ + agents?: RoomAgentDispatch[]; + /** Optional - Identity of the participant in LiveKit room */ + participantIdentity?: string; + /** Optional - Name of the participant in LiveKit room */ + participantName?: string; + /** Optional - User-defined metadata. Will be attached to a created Participant in the room. */ + participantMetadata?: string; + /** Optional - User-defined attributes. Will be attached to a created Participant in the room. */ + participantAttributes?: { [key: string]: string }; + /** Optional - Country where the call terminates as ISO 3166-1 alpha-2 */ + destinationCountry?: string; +} + +// Twilio types +export interface ConnectTwilioCallOptions { + /** The direction of the call */ + twilioCallDirection: ConnectTwilioCallRequest_TwilioCallDirection; + /** What LiveKit room should this call be connected to */ + roomName: string; + /** Optional agents to dispatch the call to */ + agents?: RoomAgentDispatch[]; + /** Optional identity of the participant in LiveKit room */ + participantIdentity?: string; + /** Optional name of the participant in LiveKit room */ + participantName?: string; + /** Optional user-defined metadata. Will be attached to a created Participant in the room. */ + participantMetadata?: string; + /** Optional user-defined attributes. Will be attached to a created Participant in the room. */ + participantAttributes?: { [key: string]: string }; + /** Country where the call terminates as ISO 3166-1 alpha-2 */ + destinationCountry?: string; +} + +/** + * Client to access Connector APIs for WhatsApp and Twilio integrations + */ +export class ConnectorClient extends ServiceBase { + private readonly rpc: Rpc; + + /** + * @param host - hostname including protocol. i.e. 'https://.livekit.cloud' + * @param apiKey - API Key, can be set in env var LIVEKIT_API_KEY + * @param secret - API Secret, can be set in env var LIVEKIT_API_SECRET + * @param options - client options + */ + constructor(host: string, apiKey?: string, secret?: string, options?: ClientOptions) { + super(apiKey, secret); + const rpcOptions = options?.requestTimeout + ? { requestTimeout: options.requestTimeout } + : undefined; + this.rpc = new TwirpRpc(host, livekitPackage, rpcOptions); + } + + /** + * Initiate an outbound WhatsApp call + * + * @param options - WhatsApp call options + * @returns Promise containing the WhatsApp call ID and room name + */ + async dialWhatsAppCall(options: DialWhatsAppCallOptions): Promise { + const whatsappBizOpaqueCallbackData = options.whatsappBizOpaqueCallbackData || ''; + const roomName = options.roomName || ''; + const participantIdentity = options.participantIdentity || ''; + const participantName = options.participantName || ''; + const participantMetadata = options.participantMetadata || ''; + const destinationCountry = options.destinationCountry || ''; + + const req = new DialWhatsAppCallRequest({ + whatsappPhoneNumberId: options.whatsappPhoneNumberId, + whatsappToPhoneNumber: options.whatsappToPhoneNumber, + whatsappApiKey: options.whatsappApiKey, + whatsappCloudApiVersion: options.whatsappCloudApiVersion, + whatsappBizOpaqueCallbackData, + roomName, + agents: options.agents, + participantIdentity, + participantName, + participantMetadata, + participantAttributes: options.participantAttributes, + destinationCountry, + }).toJson(); + + const data = await this.rpc.request( + svc, + 'DialWhatsAppCall', + req, + await this.authHeader({ roomCreate: true }), + ); + return DialWhatsAppCallResponse.fromJson(data, { ignoreUnknownFields: true }); + } + + /** + * Accept an inbound WhatsApp call + * + * @param options - WhatsApp call accept options + * @returns Promise containing the room name + */ + async acceptWhatsAppCall( + options: AcceptWhatsAppCallOptions, + ): Promise { + const whatsappBizOpaqueCallbackData = options.whatsappBizOpaqueCallbackData || ''; + const roomName = options.roomName || ''; + const participantIdentity = options.participantIdentity || ''; + const participantName = options.participantName || ''; + const participantMetadata = options.participantMetadata || ''; + const destinationCountry = options.destinationCountry || ''; + + const req = new AcceptWhatsAppCallRequest({ + whatsappPhoneNumberId: options.whatsappPhoneNumberId, + whatsappApiKey: options.whatsappApiKey, + whatsappCloudApiVersion: options.whatsappCloudApiVersion, + whatsappCallId: options.whatsappCallId, + whatsappBizOpaqueCallbackData, + sdp: options.sdp, + roomName, + agents: options.agents, + participantIdentity, + participantName, + participantMetadata, + participantAttributes: options.participantAttributes, + destinationCountry, + }).toJson(); + + const data = await this.rpc.request( + svc, + 'AcceptWhatsAppCall', + req, + await this.authHeader({ roomCreate: true }), + ); + return AcceptWhatsAppCallResponse.fromJson(data, { ignoreUnknownFields: true }); + } + + /** + * Connect an established WhatsApp call (used for business-initiated calls) + * + * @param whatsappCallId - Call ID sent by Meta + * @param sdp - Session description from Meta + */ + async connectWhatsAppCall( + whatsappCallId: string, + sdp: SessionDescription, + ): Promise { + const req = new ConnectWhatsAppCallRequest({ + whatsappCallId, + sdp, + }).toJson(); + + const data = await this.rpc.request( + svc, + 'ConnectWhatsAppCall', + req, + await this.authHeader({ roomCreate: true }), + ); + return ConnectWhatsAppCallResponse.fromJson(data, { ignoreUnknownFields: true }); + } + + /** + * Disconnect an active WhatsApp call + * + * @param whatsappCallId - Call ID sent by Meta + * @param whatsappApiKey - The API key of the business that is disconnecting the call + */ + async disconnectWhatsAppCall( + whatsappCallId: string, + whatsappApiKey: string, + ): Promise { + const req = new DisconnectWhatsAppCallRequest({ + whatsappCallId, + whatsappApiKey, + }).toJson(); + + const data = await this.rpc.request( + svc, + 'DisconnectWhatsAppCall', + req, + await this.authHeader({ roomCreate: true }), + ); + return DisconnectWhatsAppCallResponse.fromJson(data, { ignoreUnknownFields: true }); + } + + /** + * Connect a Twilio call to a LiveKit room + * + * @param options - Twilio call connection options + * @returns Promise containing the WebSocket connect URL for Twilio media stream + */ + async connectTwilioCall(options: ConnectTwilioCallOptions): Promise { + const participantIdentity = options.participantIdentity || ''; + const participantName = options.participantName || ''; + const participantMetadata = options.participantMetadata || ''; + const destinationCountry = options.destinationCountry || ''; + + const req = new ConnectTwilioCallRequest({ + twilioCallDirection: options.twilioCallDirection, + roomName: options.roomName, + agents: options.agents, + participantIdentity, + participantName, + participantMetadata, + participantAttributes: options.participantAttributes, + destinationCountry, + }).toJson(); + + const data = await this.rpc.request( + svc, + 'ConnectTwilioCall', + req, + await this.authHeader({ roomCreate: true }), + ); + return ConnectTwilioCallResponse.fromJson(data, { ignoreUnknownFields: true }); + } +} + +// Re-export enums and types for convenience +export { WhatsAppCallDirection, ConnectTwilioCallRequest_TwilioCallDirection }; diff --git a/packages/livekit-server-sdk/src/index.ts b/packages/livekit-server-sdk/src/index.ts index 800d21ec..8264df1c 100644 --- a/packages/livekit-server-sdk/src/index.ts +++ b/packages/livekit-server-sdk/src/index.ts @@ -56,6 +56,7 @@ export { } from '@livekit/protocol'; export * from './AccessToken.js'; export * from './AgentDispatchClient.js'; +export * from './ConnectorClient.js'; export * from './EgressClient.js'; export * from './grants.js'; export * from './IngressClient.js'; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0dac0036..1b34a5b2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -267,8 +267,8 @@ importers: specifier: ^1.10.1 version: 1.10.1 '@livekit/protocol': - specifier: ^1.42.0 - version: 1.42.0 + specifier: link:../../../protocol/packages/javascript + version: link:../../../protocol/packages/javascript camelcase-keys: specifier: ^9.0.0 version: 9.1.3 @@ -989,9 +989,6 @@ packages: '@livekit/mutex@1.1.1': resolution: {integrity: sha512-EsshAucklmpuUAfkABPxJNhzj9v2sG7JuzFDL4ML1oJQSV14sqrpTYnsaOudMAw9yOaW53NU3QQTlUQoRs4czw==} - '@livekit/protocol@1.42.0': - resolution: {integrity: sha512-42sYSCay2PZrn5yHHt+O3RQpTElcTrA7bqg7iYbflUApeerA5tUCJDr8Z4abHsYHVKjqVUbkBq/TPmT3X6aYOQ==} - '@livekit/typed-emitter@3.0.0': resolution: {integrity: sha512-9bl0k4MgBPZu3Qu3R3xy12rmbW17e3bE9yf4YY85gJIQ3ezLEj/uzpKHWBsLaDoL5Mozz8QCgggwIBudYQWeQg==} @@ -4409,10 +4406,6 @@ snapshots: '@livekit/mutex@1.1.1': {} - '@livekit/protocol@1.42.0': - dependencies: - '@bufbuild/protobuf': 1.10.1 - '@livekit/typed-emitter@3.0.0': {} '@manypkg/find-root@1.1.0': From ad379d191082c2e6488cba37c0bbcf9fff197ae6 Mon Sep 17 00:00:00 2001 From: Anunay Maheshwari Date: Thu, 20 Nov 2025 00:37:41 +0530 Subject: [PATCH 2/5] bump protocol --- packages/livekit-server-sdk/package.json | 2 +- pnpm-lock.yaml | 11 +++++++++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/packages/livekit-server-sdk/package.json b/packages/livekit-server-sdk/package.json index 7659da1e..717e8294 100644 --- a/packages/livekit-server-sdk/package.json +++ b/packages/livekit-server-sdk/package.json @@ -43,7 +43,7 @@ }, "dependencies": { "@bufbuild/protobuf": "^1.10.1", - "@livekit/protocol": "link:../../../protocol/packages/javascript", + "@livekit/protocol": "^1.43.1", "camelcase-keys": "^9.0.0", "jose": "^5.1.2" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1b34a5b2..31b59d5f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -267,8 +267,8 @@ importers: specifier: ^1.10.1 version: 1.10.1 '@livekit/protocol': - specifier: link:../../../protocol/packages/javascript - version: link:../../../protocol/packages/javascript + specifier: ^1.43.1 + version: 1.43.1 camelcase-keys: specifier: ^9.0.0 version: 9.1.3 @@ -989,6 +989,9 @@ packages: '@livekit/mutex@1.1.1': resolution: {integrity: sha512-EsshAucklmpuUAfkABPxJNhzj9v2sG7JuzFDL4ML1oJQSV14sqrpTYnsaOudMAw9yOaW53NU3QQTlUQoRs4czw==} + '@livekit/protocol@1.43.1': + resolution: {integrity: sha512-0NiinrCw9PruS9rFqr/F4UhLn0t09DDjSRgMnU3uu2iHT7uW4wgXPBlTp9HZ/nSShsSd0YCcG2HLX3ltwgkVcw==} + '@livekit/typed-emitter@3.0.0': resolution: {integrity: sha512-9bl0k4MgBPZu3Qu3R3xy12rmbW17e3bE9yf4YY85gJIQ3ezLEj/uzpKHWBsLaDoL5Mozz8QCgggwIBudYQWeQg==} @@ -4406,6 +4409,10 @@ snapshots: '@livekit/mutex@1.1.1': {} + '@livekit/protocol@1.43.1': + dependencies: + '@bufbuild/protobuf': 1.10.1 + '@livekit/typed-emitter@3.0.0': {} '@manypkg/find-root@1.1.0': From faa81be63fd02132d3cecd0f3e8f783892a83f09 Mon Sep 17 00:00:00 2001 From: Anunay Maheshwari Date: Sat, 6 Dec 2025 15:49:06 +0530 Subject: [PATCH 3/5] PR feedback --- packages/livekit-server-sdk/src/ConnectorClient.ts | 8 ++------ packages/livekit-server-sdk/src/index.ts | 7 +++++++ 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/packages/livekit-server-sdk/src/ConnectorClient.ts b/packages/livekit-server-sdk/src/ConnectorClient.ts index 18a16c10..4a53741a 100644 --- a/packages/livekit-server-sdk/src/ConnectorClient.ts +++ b/packages/livekit-server-sdk/src/ConnectorClient.ts @@ -14,7 +14,6 @@ import { DialWhatsAppCallResponse, DisconnectWhatsAppCallRequest, DisconnectWhatsAppCallResponse, - WhatsAppCallDirection, } from '@livekit/protocol'; import type { ClientOptions } from './ClientOptions.js'; import { ServiceBase } from './ServiceBase.js'; @@ -24,7 +23,7 @@ const svc = 'Connector'; // WhatsApp types export interface DialWhatsAppCallOptions { - /** Required - The number of the business that is initiating the call */ + /** Required - The identifier of the WhatsApp phone number that is initiating the call */ whatsappPhoneNumberId: string; /** Required - The number of the user that is supposed to receive the call */ whatsappToPhoneNumber: string; @@ -51,7 +50,7 @@ export interface DialWhatsAppCallOptions { } export interface AcceptWhatsAppCallOptions { - /** Required - The number of the business that is connecting the call */ + /** Required - The identifier of the WhatsApp phone number that is connecting the call */ whatsappPhoneNumberId: string; /** Required - The API key of the business that is connecting the call */ whatsappApiKey: string; @@ -278,6 +277,3 @@ export class ConnectorClient extends ServiceBase { return ConnectTwilioCallResponse.fromJson(data, { ignoreUnknownFields: true }); } } - -// Re-export enums and types for convenience -export { WhatsAppCallDirection, ConnectTwilioCallRequest_TwilioCallDirection }; diff --git a/packages/livekit-server-sdk/src/index.ts b/packages/livekit-server-sdk/src/index.ts index 8264df1c..0a23f12a 100644 --- a/packages/livekit-server-sdk/src/index.ts +++ b/packages/livekit-server-sdk/src/index.ts @@ -3,13 +3,19 @@ // SPDX-License-Identifier: Apache-2.0 export { + AcceptWhatsAppCallResponse, AliOSSUpload, AudioCodec, AutoParticipantEgress, AutoTrackEgress, AzureBlobUpload, + ConnectTwilioCallRequest_TwilioCallDirection, + ConnectTwilioCallResponse, + ConnectWhatsAppCallResponse, DataPacket_Kind, + DialWhatsAppCallResponse, DirectFileOutput, + DisconnectWhatsAppCallResponse, EgressInfo, EgressStatus, EncodedFileOutput, @@ -38,6 +44,7 @@ export { RoomCompositeEgressRequest, RoomEgress, S3Upload, + SessionDescription, SIPDispatchRuleInfo, SIPParticipantInfo, SIPTrunkInfo, From ad9511aa036c55fada028d02f76954c058439d64 Mon Sep 17 00:00:00 2001 From: Anunay Maheshwari Date: Sat, 6 Dec 2025 15:49:47 +0530 Subject: [PATCH 4/5] Create red-squids-double.md --- .changeset/red-squids-double.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/red-squids-double.md diff --git a/.changeset/red-squids-double.md b/.changeset/red-squids-double.md new file mode 100644 index 00000000..2372f90a --- /dev/null +++ b/.changeset/red-squids-double.md @@ -0,0 +1,5 @@ +--- +"livekit-server-sdk": minor +--- + +feat(connector): initial client impl From a0b0a791a7cb7148103de78b1e21fba2939d239e Mon Sep 17 00:00:00 2001 From: Anunay Maheshwari Date: Sun, 7 Dec 2025 00:12:22 +0530 Subject: [PATCH 5/5] type imports --- packages/livekit-server-sdk/src/ConnectorClient.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/livekit-server-sdk/src/ConnectorClient.ts b/packages/livekit-server-sdk/src/ConnectorClient.ts index 4a53741a..0500aab4 100644 --- a/packages/livekit-server-sdk/src/ConnectorClient.ts +++ b/packages/livekit-server-sdk/src/ConnectorClient.ts @@ -1,12 +1,15 @@ // SPDX-FileCopyrightText: 2025 LiveKit, Inc. // // SPDX-License-Identifier: Apache-2.0 -import type { RoomAgentDispatch, SessionDescription } from '@livekit/protocol'; +import type { + ConnectTwilioCallRequest_TwilioCallDirection, + RoomAgentDispatch, + SessionDescription, +} from '@livekit/protocol'; import { AcceptWhatsAppCallRequest, AcceptWhatsAppCallResponse, ConnectTwilioCallRequest, - ConnectTwilioCallRequest_TwilioCallDirection, ConnectTwilioCallResponse, ConnectWhatsAppCallRequest, ConnectWhatsAppCallResponse,