twilio, plivo, and nexmo are backend communication platforms that enable developers to integrate SMS, voice calls, and verification services into web and mobile applications. twilio is the market leader with a vast ecosystem of communication tools. plivo focuses on cost-effective global messaging and voice with a simpler API surface. nexmo was a major player acquired by Vonage, and its original npm package is now deprecated in favor of the Vonage Server SDK. All three provide Node.js libraries to interact with their REST APIs, but they differ in maintenance status, feature depth, and pricing models.
When building applications that need to send texts, make calls, or verify users, you typically rely on a Communication Platform as a Service (CPaaS). twilio, plivo, and nexmo have been the top contenders in this space for years. However, their current status and technical approaches differ significantly. Let's break down how they handle core tasks and what that means for your architecture.
This is the most critical factor for long-term projects.
twilio is actively maintained.
twilio npm package receives regular updates.// twilio: Actively maintained
const twilio = require('twilio');
const client = twilio(accountSid, authToken);
plivo is actively maintained.
plivo npm package is stable and updated.// plivo: Actively maintained
const plivo = require('plivo');
const client = new plivo.Client(authId, authToken);
nexmo is deprecated.
nexmo package is no longer updated.@vonage/server-sdk.nexmo in new projects introduces security and compatibility risks.// nexmo: DEPRECATED - Do not use in new projects
const Nexmo = require('nexmo');
const nexmo = new Nexmo(key, secret);
// Migration path: npm install @vonage/server-sdk
All three platforms allow you to send text messages, but the method signatures and object structures vary.
twilio uses a resource-based approach.
.messages on the client instance.from, to, and body are explicit.// twilio: Send SMS
await client.messages.create({
body: 'Hello from Twilio',
from: '+1234567890',
to: '+0987654321'
});
plivo uses a similar resource structure but with different parameter names.
.messages on the client instance.src and dst instead of from and to.// plivo: Send SMS
await client.messages.create({
text: 'Hello from Plivo',
src: '+1234567890',
dst: '+0987654321'
});
nexmo (Legacy) used a callback-heavy style originally, though promises were added later.
.message on the instance.sendSms method with positional arguments or an object.// nexmo: Send SMS (Legacy)
const message = {
from: 'Vonage',
to: '+0987654321',
text: 'Hello from Nexmo'
};
nexmo.message.sendSms(message.from, message.to, message.text, (err, responseData) => {
if (err) { console.log(err); }
});
Voice APIs are more complex because they involve call flow control (what happens when someone answers).
twilio uses TwiML to define call behavior.
// twilio: Make Voice Call
await client.calls.create({
url: 'http://example.com/call-control.xml',
from: '+1234567890',
to: '+0987654321'
});
plivo uses Plivo XML (similar to TwiML).
// plivo: Make Voice Call
await client.calls.create({
answer_url: ['http://example.com/call-control.xml'],
from: '+1234567890',
to: '+0987654321'
});
nexmo (Legacy) used the Voice API with JSON call objects.
// nexmo: Make Voice Call (Legacy)
const call = {
to: [{ type: 'phone', number: '+0987654321' }],
from: { type: 'phone', number: '+1234567890' },
ncco: [{ action: 'talk', text: 'Hello' }]
};
nexmo.voice.createCall(call, (err, res) => {
if (err) { console.log(err); }
});
Sending one-time passwords (OTP) is a common use case. All three have dedicated APIs for this.
twilio has a dedicated verify service.
// twilio: Send Verification Code
await client.verify.v2.services('VAxxxx').verifications.create({
to: '+0987654321',
channel: 'sms'
});
plivo offers a Verify API.
// plivo: Send Verification Code
await client.verify.send_verification({
recipient: '+0987654321',
channel: 'sms'
});
nexmo (Legacy) had a Verify API.
// nexmo: Send Verification Code (Legacy)
nexmo.verify.request({
number: '+0987654321',
brand: 'MyApp'
}, (err, result) => {
if (err) { console.log(err); }
});
How each library handles network failures and API errors impacts your code stability.
twilio throws standard JavaScript errors.
// twilio: Error Handling
try {
await client.messages.create({ ... });
} catch (error) {
console.log(`Twilio Error ${error.code}: ${error.message}`);
}
plivo throws errors with response details.
// plivo: Error Handling
try {
await client.messages.create({ ... });
} catch (error) {
console.log(`Plivo Error: ${error.message}`);
}
nexmo (Legacy) used callbacks or promises inconsistently.
// nexmo: Error Handling (Legacy)
nexmo.message.sendSms(..., (err, result) => {
if (err) {
console.log(`Nexmo Error: ${err}`);
}
});
| Feature | twilio | plivo | nexmo |
|---|---|---|---|
| Status | β Active | β Active | β Deprecated |
| SMS Params | from, to, body | src, dst, text | from, to, text |
| Voice Control | TwiML (XML) | Plivo XML | NCCO (JSON) |
| Verify API | β Advanced | β Basic | β Legacy |
| Error Style | Standard Errors | Standard Errors | Callbacks/Mixed |
| Replacement | N/A | N/A | @vonage/server-sdk |
twilio is the safe bet for most teams. It costs more, but the reliability, documentation, and feature depth save engineering time. Use it for critical communication flows where failure is not an option.
plivo is the budget-friendly alternative. It works well for high-volume SMS or simple voice needs where you don't need advanced IVR or video. It is a solid choice for cost-sensitive startups.
nexmo should be avoided. If you see this package in a codebase, plan a migration to the Vonage Server SDK immediately. Using deprecated communication libraries poses security risks for user data and verification flows.
Final Thought: For new projects, choose between twilio and plivo based on budget and feature needs. Never start a new project with nexmo.
Do NOT choose nexmo for new projects. The nexmo npm package is deprecated and has been replaced by the Vonage Server SDK (@vonage/server-sdk). Existing projects using nexmo should plan a migration to Vonage to ensure security updates and continued support. Treat this package as legacy technology.
Choose plivo if cost is a primary concern and you need high-volume SMS or voice capabilities without extra features. It offers competitive pricing and a straightforward API that works well for simple use cases. It is suitable for startups or projects where budget efficiency outweighs the need for a broad feature set.
Choose twilio if you need the most reliable infrastructure with extensive documentation and a wide range of features like video, verify, and programmable voice. It is ideal for enterprise projects where uptime and support are critical, despite potentially higher costs. The ecosystem is mature, making it easier to find help and integrations.
A Node.JS REST API Wrapper library for Nexmo.
For full API documentation refer to developer.nexmo.com.
Installation | Constructor | Callbacks | Messaging | Message Signing | Voice | Verify | Number Insight | Applications | Management | Redact | Pricing | JWT (JSON Web Token)
npm install nexmo
const Nexmo = require('nexmo');
const nexmo = new Nexmo({
apiKey: API_KEY,
apiSecret: API_SECRET,
applicationId: APP_ID,
privateKey: PRIVATE_KEY_PATH,
signatureSecret: SIGNATURE_SECRET,
signatureMethod: SIGNATURE_METHOD
}, options);
apiKey - API Key from Nexmo. If applicationId and privateKey are present, apiKey is optional.apiSecret - API SECRET from Nexmo. If applicationId and privateKey are present, apiSecret is optional.applicationId - (optional) The Nexmo Application ID to be used when creating JWTs.privateKey - (optional) The Private Key to be used when creating JWTs. You can specify the key as any of the following:
signatureSecret - (optional) API singature secret from Nexmo, used for signing SMS message requestssignatureMethod - (optional) singature method matching the one you gave Nexmo, used for signing SMS message requests. Must be one of "md5hash", "md5", "sha1", "sha256", or "sha512"options - (optional) Additional options for the constructor.Options are:
{
// If true, log information to the console
debug: true|false,
// append info the the User-Agent sent to Nexmo
// e.g. pass 'my-app' for /nexmo-node/1.0.0/4.2.7/my-app
appendToUserAgent: string,
// Set a custom logger
logger: {
log: function() {level, args...}
info: function() {args...},
warn: function() {args...}
},
// Set a custom timeout for requests to Nexmo in milliseconds. Defaults to the standard for Node http requests, which is 120,000 ms.
timeout: integer,
// Set a custom host for requests instead of api.nexmo.com
apiHost: string,
// Set a custom host for requests instead of rest.nexmo.com
restHost: string
}
All methods expect a callback function to be passed in, with a method signature of (error, response) where:
error - is an Error object if the API call returns an error, or null if the API call was successful.response - is an Object, with the API response if the API call was successful, or null if there was an error.Example:
callback = (error, response) => {
if (error) {
console.error(error)
}
if (response) {
console.log(response)
}
}
nexmo.message.sendSms(sender, recipient, message, options, callback);
options - parameter is optional. See SMS API Referencenexmo.message.sendBinaryMessage(fromnumber, tonumber, body, udh, callback);
body - Hex encoded binary dataudh - Hex encoded udhnexmo.message.sendWapPushMessage(fromnumber, tonumber, title, url, validity, callback);
validity - is optional (if given should be in milliseconds)nexmo.message.shortcodeAlert(recipient, messageParams, opts, callback);
For detailed information please see the documentation at https://developer.nexmo.com/api/voice
Requires applicationId and privateKey to be set on the constructor.
nexmo.calls.create({
to: [{
type: 'phone',
number: TO_NUMBER
}],
from: {
type: 'phone',
number: FROM_NUMBER
},
answer_url: [ANSWER_URL]
}, callback);
For more information see https://developer.nexmo.com/api/voice#createCall
nexmo.calls.get(callId, callback);
For more information see https://developer.nexmo.com/api/voice#getCall
nexmo.calls.get({status: 'completed'}, callback);
The first parameter can contain many properties to filter the returned call or to page results. For more information see the Calls API Reference.
nexmo.calls.update(callId, { action: 'hangup' }, callback);
For more information see https://developer.nexmo.com/api/voice#updateCall
nexmo.calls.stream.start(
callId,
{
stream_url: [
'https://nexmo-community.github.io/ncco-examples/assets/voice_api_audio_streaming.mp3'
],
loop: 1
});
For more information see https://developer.nexmo.com/api/voice#startStream
nexmo.calls.stream.stop(callId);
For more information see https://developer.nexmo.com/api/voice#stopStream
nexmo.calls.talk.start(
callId,
{
text: 'No songs detected',
voiceName: 'Emma',
loop: 1
}
);
For more information see https://developer.nexmo.com/api/voice#startTalk
nexmo.calls.talk.stop(callId);
For more information see https://developer.nexmo.com/api/voice#stopTalk
nexmo.calls.dtmf.send(callId, params, callback);
For more information see https://developer.nexmo.com/api/voice#startDTMF
For detailed information please see the documentation at https://developer.nexmo.com/voice/voice-api/guides/recording
nexmo.files.get(fileIdOrUrl, callback);
nexmo.files.save(fileIdOrUrl, file, callback);
nexmo.verify.request({number:<NUMBER_TO_BE_VERIFIED>,brand:<NAME_OF_THE_APP>},callback);
For more information check the documentation at https://developer.nexmo.com/api/verify#verify-request
nexmo.verify.psd2({number:<NUMBER_TO_BE_VERIFIED>,payee:<NAME_OF_THE_SELLER>,amount:<AMOUNT_IN_EUROS>},callback);
For more information check the documentation at https://developer.nexmo.com/api/verify#verifyRequestWithPSD2
nexmo.verify.check({request_id:<UNIQUE_ID_FROM_VERIFICATION_REQUEST>,code:<CODE_TO_CHECK>},callback);
For more information check the documentation at https://developer.nexmo.com/api/verify#verify-check
nexmo.verify.search(<ONE_REQUEST_ID or ARRAY_OF_REQUEST_ID>,callback);
For more information check the documentation at https://developer.nexmo.com/api/verify#verify-search
nexmo.verify.control({request_id:<UNIQUE_ID_FROM_VERIFICATION_REQUEST>,cmd:'cancel'},callback);
For more information check the documentation at https://developer.nexmo.com/api/verify#verify-control
nexmo.verify.control({request_id:<UNIQUE_ID_FROM_VERIFICATION_REQUEST>,cmd:'trigger_next_event'},callback);
For more information check the documentation at https://developer.nexmo.com/api/verify#verify-control
nexmo.numberInsight.get({level: 'basic', number: NUMBER}, callback);
For more information check the documentation at https://developer.nexmo.com/number-insight/building-blocks/number-insight-basic/node
Example:
nexmo.numberInsight.get({level: 'basic', number: '1-234-567-8900'}, callback);
nexmo.numberInsight.get({level: 'standard', number: NUMBER}, callback);
For more information check the documentation at https://developer.nexmo.com/number-insight/building-blocks/number-insight-standard/node
Example:
nexmo.numberInsight.get({level: 'standard', number: '1-234-567-8900'}, callback);
nexmo.numberInsight.get({level: 'advancedSync', number: NUMBER}, callback);
For more information check the documentation at https://developer.nexmo.com/number-insight/building-blocks/number-insight-advanced/node
Number Insight Advanced might take a few seconds to return a result, therefore the option exists to process the result asynchronously through a webhook.
nexmo.numberInsight.get({level: 'advancedAsync', number: NUMBER, callback: "http://example.com"}, callback);
In this case, the result of your insight request is posted to the callback URL as a webhook. For more details on webhooks see the Number Insight Advanced documentation.
For an overview of applications see https://developer.nexmo.com/concepts/guides/applications
nexmo.applications.create(params, callback);
For more information see https://developer.nexmo.com/api/application.v2#createApplication
params can be
{
"name": "My Application",
"capabilities": {
"voice": {
"webhooks": {
"answer_url": {
"address": "https://example.com/webhooks/answer",
"http_method": "POST"
},
"event_url": {
"address": "https://example.com/webhooks/event",
"http_method": "POST"
}
}
},
"messages": {
"webhooks": {
"inbound_url": {
"address": "https://example.com/webhooks/inbound",
"http_method": "POST"
},
"status_url": {
"address": "https://example.com/webhooks/status",
"http_method": "POST"
}
}
},
"rtc": {
"webhooks": {
"event_url": {
"address": "https://example.com/webhooks/event",
"http_method": "POST"
}
}
},
"vbc": {}
}
}
nexmo.applications.get(appId, callback, v2flag);
For more information see https://developer.nexmo.com/api/application.v2#getApplication
v2flag - if true, you'll receive the V2 API response, else you'll receive a V1 style response from the V2 APInexmo.applications.get(options, callback, v2flag);
For more information see https://developer.nexmo.com/api/application.v2#listApplication
options - filter options, use {} to get all your applicationsv2flag - if true, you'll receive the V2 API response, else you'll receive a V1 style response from the V2 APInexmo.applications.update(appId, params, callback);
For more information see https://developer.nexmo.com/api/application.v2#updateApplication
nexmo.application.delete(appId, callback);
For more information see https://developer.nexmo.com/api/application.v2#deleteApplication
nexmo.account.checkBalance(callback);
nexmo.account.listSecrets(apiKey, callback);
nexmo.account.getSecret(apiKey, secretId, callback);
nexmo.account.createSecret(apiKey, secret, callback);
nexmo.account.deleteSecret(apiKey, secretId, callback);
nexmo.number.getPricing(countryCode, callback);
countryCode - 2 letter ISO Country Codenexmo.number.getPhonePricing(product, msisdn, callback);
product - either voice or smsmsisdn - Mobile Station International Subscriber Directory Number (MSISDN) is a number used to identify a mobile phone number internationally. i.e. 447700900000nexmo.number.get(options, callback);
options parameter is an optional Dictionary Object containing any of the following parameters
patternsearch_patternindexsizehas_applicationapplication_idFor more details about these options, refer to the Numbers API reference
Example:
nexmo.number.get({pattern:714,index:1,size:50,search_pattern:2}, callback);
nexmo.number.search(countryCode,options,callback);
options parameter is optional. They can be one of the following :
patternsearch_patterntypefeaturesindexsizeFor more details about these options, refer to the Numbers API reference
Example:
nexmo.number.search('US',{pattern:3049,index:1,size:50,type:'mobile-lvn',features:'VOICE',search_pattern:2}, callback);
nexmo.number.buy(countryCode, msisdn, callback);
// optional target_api_key option
nexmo.number.buy(countryCode, msisdn, target_api_key, callback);
For more details on these parameters, see the Numbers API reference.
nexmo.number.cancel(countryCode, msisdn, callback);
// optional target_api_key option
nexmo.number.cancel(countryCode, msisdn, target_api_key, callback);
For more details on these parameters, see the Numbers API reference.
nexmo.number.update(countryCode, msisdn, params, callback);
params is a dictionary of parameters as described in the Numbers API reference.
nexmo.account.updatePassword(<NEW_PASSWORD>,callback);
nexmo.account.updateSMSCallback(<NEW_CALLBACK_URL>,callback);
nexmo.account.updateDeliveryReceiptCallback(<NEW_DR_CALLBACK_URL>,callback);
nexmo.redact.transaction(id, type, callback);
type is the type of service you wish to retrieve pricing for: either sms, sms-transit or voice.
nexmo.pricing.get(type, country_code, callback);
nexmo.pricing.getFull(type, callback);
nexmo.pricing.getPrefix(type, country_prefix, callback);
nexmo.pricing.getPhone(type, phone, callback);
nexmo.media.upload({"file": "/path/to/file"}, callback);
nexmo.media.upload({"url": "https://example.com/ncco.json"}, callback);
// See https://ea.developer.nexmo.com/api/media#search-media-files
// for possible search parameters
nexmo.media.search({ page_size: 1, page_index: 1 }, callback);
nexmo.media.download(id, callback);
nexmo.media.delete(id, callback);
nexmo.media.update(id, body, callback);
nexmo.media.get(id, callback);
There are two ways of generating a JWT. You can use the function that exists on the Nexmo definition:
const Nexmo = require('nexmo');
const jwt = Nexmo.generateJwt('path/to/private.key', {application_id: APP_ID});
Or via a Nexmo instance where your supplied applicationId and privateKey credentials will be used:
const Nexmo = require('nexmo');
const nexmo = new Nexmo({
apiKey: API_KEY,
apiSecret: API_SECRET,
applicationId: APP_ID,
privateKey: PRIVATE_KEY_PATH,
});
const jwt = nexmo.generateJwt();
There are two ways of generating a signature hash. Both strip the sig parameter if supplied. You can use the function that exists on the Nexmo definition:
const Nexmo = require('nexmo');
const hash = Nexmo.generateSignature(SIGNATURE_METHOD, SIGNATURE_SECRET, params);
Or via a Nexmo instance where your supplied signatureSecret and signatureMethod:
const Nexmo = require('nexmo');
const nexmo = new Nexmo({
apiKey: API_KEY,
apiSecret: API_SECRET,
signatureSecret: SIGNATURE_SECRET,
signatureMethod: SIGNATURE_METHOD,
});
const hash = nexmo.generateSignature();
SIGNATURE_METHOD is the signature method matching the one you gave Nexmo. Must be one of "md5hash", "md5", "sha1", "sha256", or "sha512".
nexmo.voice.sendTTSMessage(<TO_NUMBER>,message,options,callback);
nexmo.sendTTSPromptWithCapture(<TO_NUMBER>,message,<MAX_DIGITS>, <BYE_TEXT>,options,callback);
nexmo.voice.sendTTSPromptWithConfirm(<TO_NUMBER>, message ,<MAX_DIGITS>,'<PIN_CODE>',<BYE_TEXT>,<FAILED_TEXT>,options,callback);
Run:
npm test
Or to continually watch and run tests as you change the code:
npm run-script test-watch
See examples/README.md.
Also, see the Nexmo Node Quickstarts repo.
IMPORTANT
This section uses internal APIs and should not be relied on. We make no guarantees that the interface is stable. Relying on these methods is not recommended for production applications
For endpoints that are not yet implemented, you can use the Nexmo HTTP Client to make requests with the correct authentication method.
In these examples, we assume that you've created a nexmo instance as follows:
const nexmo = new Nexmo({
apiKey: 'API_KEY',
apiSecret: 'API_SECRET',
applicationId: 'APPLICATION_ID',
privateKey: './private.key',
});
api.nexmo.com, use the nexmo.options.api object.rest.nexmo.com, use the nexmo.options.rest object.Both of these objects expose the following methods:
get(path, params, callback, useJwt) (params is the query string to use)post(path, params, callback, useJwt) (params is the POST body to send)postUseQueryString(path, params, callback, useJwt) (params is the query string to use)delete(path, callback, useJwt)To make a request to api.nexmo.com/v1/calls?status=rejected:
nexmo.options.api.get(
"/v1/calls",
{"status": "rejected"},
function(err, data){
console.log(err);
console.log(data);
},
true // Use JWT for authentication
);
To make a request to rest.nexmo.com/sms/json?from=Demo&to=447700900000&text=Testing:
nexmo.options.rest.postUseQueryString(
"/sms/json",
{"from": "Demo", "to": "447700900000", "text": "Testing"},
function(err, data){
console.log(err);
console.log(data);
},
false // Don't use JWT, fall back to API key/secret
);
MIT - see LICENSE