Guide · Twilio

Migrate Twilio Node.js SDK (twilio) from v3 to v4

twilio 4.0.0 rewrote the Node.js helper library in TypeScript and turned lazy loading on. Most REST calls are untouched; the breaking changes sit in access tokens, TwiML SSML helpers, two TaskRouter calls and response types.

4.0.0 was published to npm on January 25, 2023, the date of its entry in Twilio's UPGRADE.md. Twilio's changelog for the release says the source moved to TypeScript, that removing large dependencies cut the bundle size by 31%, that lazy loading is now on by default, and that the library adopted Twilio's OpenAPI specification. The list of breaking changes is short, but each item fails differently: some throw on the first request, some change XML output, some only show up in TypeScript. Later majors are lighter: UPGRADE.md says 5.0.0 has no breaking changes to existing APIs, and 6.0.0 only raises the minimum Node.js version to 20. This guide follows UPGRADE.md and Twilio's release notes, linked at the end.

What changed

  • Node.js 14 or later is required. Support for older versions was dropped, and Node.js 18 support was added.
  • Lazy loading of Twilio modules is on by default; pass lazyLoading: false to turn it off.
  • new AccessToken(...) requires an identity in its options.
  • IpMessagingGrant is removed and ConversationsGrant is deprecated.
  • The ssml* helpers on <Say> are renamed, for example say.ssmlEmphasis() to say.emphasis(), and Refer.referSip() becomes Refer.sip().
  • TaskRouter cumulative and real-time Workers statistics no longer accept a Worker SID.
  • Some response properties use Record types with string keys instead of object, including subresourceUris and links.

Migration steps

  1. Check your Node.js version and upgrade

    npm install twilio@^4

    Check the runtime first: 4.x needs Node.js 14 or later, and the 4.x README lists 14, 16 and 18 as supported. If you plan to go straight to the latest release, 6.x needs Node.js 20, so you may as well upgrade Node once. When 4.x shipped, Twilio's changelog noted that it was not compatible with Twilio Functions; if you deploy there, check the current Functions runtime notes before upgrading. The main entry points (require("twilio")(accountSid, authToken), client.messages.create(...)) are not on UPGRADE.md's list of changes.

  2. Pass identity when you create access tokens

    Before

    const AccessToken = require("twilio").jwt.AccessToken;
    
    const token = new AccessToken(accountSid, apiKeySid, apiKeySecret);
    token.identity = "alice";

    After

    const AccessToken = require("twilio").jwt.AccessToken;
    
    const token = new AccessToken(accountSid, apiKeySid, apiKeySecret, {
      identity: "alice",
    });

    In 3.x identity was optional and could be assigned after construction. In 4.x the constructor throws identity is required to be specified in options when it is missing. Token endpoints usually run per request, so this fails the first time a client asks for a token, not when the server starts.

  3. Replace removed grants

    Before

    token.addGrant(new AccessToken.IpMessagingGrant({ serviceSid }));

    After

    token.addGrant(new AccessToken.ChatGrant({ serviceSid }));

    IpMessagingGrant is removed; the 3.x deprecation warning already pointed to ChatGrant. ConversationsGrant is listed as deprecated, and neither class appears in the current AccessToken source. UPGRADE.md names VoiceGrant as its replacement while the 3.x deprecation warning named VideoGrant, so pick the grant that matches what the token is for.

  4. Rename SSML helpers in TwiML

    Before

    const response = new VoiceResponse();
    const say = response.say("Hello");
    say.ssmlEmphasis("you");
    say.ssmlBreak({ time: "500ms" });

    After

    const response = new VoiceResponse();
    const say = response.say("Hello");
    say.emphasis("you");
    say.break({ time: "500ms" });

    The full list of renames on <Say> and <Refer>:

    • ssmlBreak() and break_() to break()
    • ssmlEmphasis() to emphasis(), ssmlLang() to lang(), ssmlP() to p()
    • ssmlPhoneme() to phoneme(), ssmlProsody() to prosody(), ssmlS() to s()
    • ssmlSayAs() to sayAs(), ssmlSub() to sub(), ssmlW() to w()
    • Refer.referSip() to Refer.sip()
  5. Update TaskRouter worker statistics

    Before

    client.taskrouter.v1.workspaces("WS...").workers("WK...").cumulativeStatistics();
    client.taskrouter.v1.workspaces("WS...").workers("WK...").realTimeStatistics();

    After

    client.taskrouter.v1.workspaces("WS...").workers.cumulativeStatistics();
    client.taskrouter.v1.workspaces("WS...").workers.realTimeStatistics();

    These operations map to GET /v1/Workspaces/{WorkspaceSid}/Workers/CumulativeStatistics and .../RealTimeStatistics. Code that passed a Worker SID to them needs to drop it.

  6. Review lazy loading and response types

    3.x loaded every module up front unless you passed lazyLoading: true; 4.x does the reverse. That option can simply be deleted. If you relied on eager loading, for example to pay the loading cost at startup rather than on the first request, turn it off explicitly:

    const client = require("twilio")(accountSid, authToken, {
      lazyLoading: false,
    });

    In TypeScript, some response properties changed from object to Record types with string keys, including subresourceUris on v2010 APIs and links elsewhere. Code that indexes them usually gets simpler, but explicit annotations or casts written against the old types may need updating.

How to find every affected call

Every breaking change has a distinctive call shape, so ast-grep can find them, including calls split across lines:

sg run --pattern 'new AccessToken($SID, $KEY, $SECRET)' --lang js
sg run --pattern '$TOKEN.identity = $ID' --lang js
sg run --pattern 'new $NS.IpMessagingGrant($$$ARGS)' --lang js
sg run --pattern '$SAY.ssmlEmphasis($$$ARGS)' --lang js
sg run --pattern '$REFER.referSip($$$ARGS)' --lang js
sg run --pattern '$WORKERS($SID).cumulativeStatistics($$$ARGS)' --lang js

Repeat the SSML search for each renamed helper you use, and the last pattern for realTimeStatistics. If you destructure grants (const { IpMessagingGrant } = AccessToken), search for new IpMessagingGrant($$$ARGS) as well. Run each pattern with --lang ts too. $TOKEN.identity matches any object with an identity property, so check the hits.

Testing the change

  • Run CI on the Node.js version you deploy, at least 14 for 4.x and 20 if you continue to 6.x.
  • Generate an access token in a unit test and decode it to assert the identity and grants, since a missing identity throws only at runtime.
  • Compare TwiML output: call response.toString() on your voice responses before and after the upgrade and diff the XML for every SSML helper you renamed.
  • Run tsc --noEmit to catch code written against the old object types.

Automating it

We found no official codemod for this upgrade in Twilio's UPGRADE.md, changelog or release announcement. The breaking changes are few enough that UPGRADE.md lists them one by one, which makes them good targets for search and replace, with the exception of the access token change.

Versyka finds these call sites deterministically with ast-grep, rewrites them together with the code around them (token construction, TwiML builders, mocks), then runs your repository's install, typecheck, build and tests in a disposable sandbox. A pull request is opened only when those pass.

Was this guide helpful?

Check your own repository

Paste your package.json, requirements.txt or pyproject.toml into the free upgrade check to see which API SDKs are behind, with no account. Or let Versyka run this migration on one repository and open a pull request that has already passed your tests.

Sources