Rate this page:

Migrating to Voice JavaScript SDK 2.0

Version 2.1.0 of the Voice JavaScript SDK has been released. After you've used this migration guide to upgrade to version 2.0.1, go to the Changelog Page to see what's new in version 2.1.0.

You can view details of SDK updates in the GitHub Repository's Changelog. You'll find the full, autogenerated API documentation in the GitHub pages site.

In this guide, you'll see references to a Call class. This is the renamed Connection class. You'll find more details in the guide below.

Use the new NPM module instead of the CDN

The Voice JavaScript SDK is published under @twilio/voice-sdk. The 1.x SDKs will still be available as twilio-client.

In the interest of promoting best practices, 2.0 and newer releases will not be uploaded to the Twilio CDN. Existing versions (prior to 2.0) and any further 1.x releases will continue to be served via the CDN. However, we still strongly recommend not to link directly to these files in production as it introduces the potential for interruption due to CDN outages.

For customers currently using the CDN, there are a couple of options for switching:

  1. If your project uses NPM, install the latest version of @twilio/voice-sdk. From here, the built distribution files will be available under node_modules/@twilio/voice-sdk/dist.
  2. If your project does not use NPM, it’s possible to download the latest built artifact and serve it from your own servers. Each release will have its own tag on GitHub. Download the latest release (the newest tag without -rc), unzip it, and navigate to /dist for the twilio.js and twilio.min.js files.

Stop using the Device singleton and instead instantiate a new Device instance

The singleton behavior of Device has been removed in favor of best practices. device.setup() will no longer work; instead, a new Device instance must be instantiated via new Device(token, options?).

const device = new Device(token, deviceOptions);

Stop using device.setup() and instead use device.updateOptions()

device.setup() will no longer work for creating a new Device instance.

Additionally, device.setup() will no longer work for updating the Device instance after it has been created. Instead, use device.updateOptions(deviceOptions) to update any of the Device instance's options after a new Device instance has been created.

Stop listening for device.on('ready')

Device instances are now lazy when opening the signaling connection. This means that the Device constructor is synchronous and will not cause a signaling channel to be opened.

The signaling websocket will be opened when either:

If your application needs to monitor the Device instance's registration status, the following events are emitted by the Device instance:

To monitor signaling connection status during an oubound call when not registered (you did not invoke device.register() to allow the Device instance to receive incoming calls), you should monitor the Call status.

The full list of Device events is now:

enum Device.EventName {
  Error = 'error',
  Incoming = 'incoming',
  Unregistered = 'unregistered',
  Registering = 'registering',
  Registered = 'registered',
  Destroyed = 'destroyed',

Make sure the AccessToken is kept up to date

Now that signaling is lazy, the AccessToken must be kept updated with device.updateToken(accessToken) prior to registering (with device.register())or placing an outbound Call (with device.connect()).

An expired AccessToken will prevent Voice Insights statistics from posting. This is an existing behavior from 1.x. In the future, as we explore signaling reconnection support, keeping this token updated will be even more important in order to automatically restore connection to a Call or automatically re-register the endpoint.

Below is an example of how to keep the AccessToken up to date:

const ttl = 600000; // 10 minutes
const refreshBuffer = 30000; // 30 seconds

const token = await getTokenViaAjax({ ttl });
const device = new Device(token);

setInterval(async () => {
  const newToken = await getTokenViaAjax({ ttl });
}, ttl - refreshBuffer); // Gives us a generous 30-second buffer

Update references to Device state

The Device states have changed to:

namespace Device {
  isBusy: boolean;
  enum State {
    Unregistered = 'unregistered';
    Registering = 'registering';
    Registered = 'registered';
    Destroyed = 'destroyed';

We no longer represent busy state as part of the Device state; we’ve moved it to the device.isBusy boolean.

The ready state is now represented by the Registered Device state.

The offline state is now represented by the Unregistered Device state.

An important note here is that ready and offline have always indicated registration status rather than signaling connection status. This change in version 2 aims to clear up this confusion.

Update device.connect() and call.accept() arguments

The AcceptOptions object passed to call.accept() now has the following properties:

interface Call.AcceptOptions {
    * An RTCConfiguration to pass to the RTCPeerConnection constructor. 
  rtcConfiguration?: RTCConfiguration;

   * MediaStreamConstraints to pass to getUserMedia when making or accepting a Call. 
  rtcConstraints?: MediaStreamConstraints;

The ConnectOptions object passed to device.connect() extends the AcceptOptions interface and adds an optional params property:

interface Device.ConnectOptions extends Call.AcceptOptions {
  * A flat object containing key:value pairs to be sent to the TwiML app. 
  params?: Record<string, string>;

Note that objects can now take MediaStreamConstraints. For example:

device.connect({ To: 'client:alice' }, { deviceId: 'default' });

might be re-written as:

  params: { To: 'client:alice' },
  rtcConstraints: { audio: { deviceId: 'default' } },

Update error handlers

For backward compatibility, the new error format was attached to the old format under error.twilioError:

class oldError extends Error {
  code: number;
  message: string;
  twilioError: TwilioError;

The new error format is a TwilioError object:

class TwilioError extends Error {
   * A list of possible causes for the Error.
  causes: string[];
   * The numerical code associated with this Error.
  code: number;
   * A description of what the Error means.
  description: string;
   * An explanation of when the Error may be observed.
  explanation: string;
   * Any further information discovered and passed along at run-time.
  message: string;
   * The name of this Error.
  name: string;
   * The original Error received from the external system, if any.
  originalError?: Error;
   * A list of potential solutions for the Error.
  solutions: string[];

If you are already using the new format, the migration should be straightforward:

// changing...
device.on('error', e => {

// to...
device.on('error', e => {

Otherwise, some of the error codes and messages may have changed from the old platform-specific codes to the new codes which are more consistent with Twilio, our REST API, and other SDKs.

Update error codes

With the transition to using the TwilioError format, the following error codes have changed:

Old Code New Code Description
31003 53405 When ICE connection fails
31201 31402 When getting user media fails
31208 31401 When user denies access to user media
31901 53000 When the websocket times out in preflight

Update Call logic to include ringing states if needed

In a prior patch, we included ringing states on Call, which were enabled behind the DeviceOptions property enableRingingState. We have removed this flag in 2.0, as it is now on by default.

For full functionality, this still requires setting answerOnBridge in your TwiML. However, even without answerOnBridge, the Call will go through the new ringing state (briefly, for a few ms) before getting to open. When answerOnBridge is enabled, the ringing state will begin when the recipient’s phone begins ringing and transition to open when they have answered and media is established.

This shouldn’t affect most applications, however it may be necessary for your application to know about the Call.Ringing state and ringing event, depending on your implementation.

Update removed methods and options

  • call.mediaStream
  • call.message
  • call.options
    • Was intended to be a private property
  • call.pstream
    • Was intended to be a private property
  • call.sendHangup()
    • Was intended to be a private method
  • deviceOptions.enableIceRestart
    • Now defaults to true
  • deviceOptions.enableRingingState
    • Now defaults to true
  • deviceOptions.fakeLocalDtmf
    • Now defaults to true
  • device.activeCall
    • The application should maintain references to Call instances returned in device.on('incoming') event handlers or by device.connect()

'cancel' event no longer emitted by Call instance in response to a local call.ignore()

The Call instance no longer emits the 'cancel' event in response to a local call.ignore(). Instead, it will only fire when the remote end has canceled.

Update your DeviceOptions

The new DeviceOptions interface is:

interface Device.Options {
  allowIncomingWhileBusy?: boolean;
  appName?: string;
  appVersion?: string;
  closeProtection?: boolean | string;
  codecPreferences?: Connection.Codec[];
  disableAudioContextSounds?: boolean;
  dscp?: boolean;
  edge?: string[] | string;
  forceAggressiveIceNomination?: boolean;
  logLevel: number;
	maxAverageBitrate?: number;
  sounds?: Partial<Record<Device.SoundName, string>>;

(Optional) Rename Connection to Call

We’ve renamed the Connection class to Call. This shouldn’t affect any public API; however, some internal method names have been updated. Therefore, any code reaching into the internals will break if not updated.

Rate this page:

Need some help?

We all do sometimes; code is hard. Get help now from our support team, or lean on the wisdom of the crowd by visiting Twilio's Stack Overflow Collective or browsing the Twilio tag on Stack Overflow.

Loading Code Sample...

        Thank you for your feedback!

        Please select the reason(s) for your feedback. The additional information you provide helps us improve our documentation:

        Sending your feedback...
        🎉 Thank you for your feedback!
        Something went wrong. Please try again.

        Thanks for your feedback!