Skip to content
SMS SDK
Esc
↑↓navigate↵open⌘Jpreview
On this page

Plivo

Send and receive SMS and MMS through the Plivo Message API with the SMS SDK plivo() adapter: setup, senders, webhooks, errors, and partial-support limits.

The plivo() adapter sends SMS and MMS through the Plivo Message API and parses Plivo’s V2-signed message callbacks.

To compare providers, see the Providers overview.

Support status

Partial. The adapter works and passes the shared contract tests. Two gaps in Plivo’s documentation limit it:

  • Plivo does not document its API error body. SMS SDK can tell apart only 401, which is auth, and a 429 that carries Plivo’s api_id, which is rate_limited. Every other 4xx is a request rejection and never falls back, even when the real cause is a sender or account problem another provider could avoid.
  • Plivo signs message callbacks with its V2 scheme, which covers the URL and a nonce but not the body or a timestamp. A captured callback can be replayed, or its body altered, and still pass verification. Plivo documents its V3 scheme, which signs parameters, for Voice only.

The same notes are in adapter.support.notes at runtime.

Installation

npm install @opencoredev/sms-sdk
pnpm add @opencoredev/sms-sdk
yarn add @opencoredev/sms-sdk
bun add @opencoredev/sms-sdk
nub add @opencoredev/sms-sdk
aube add @opencoredev/sms-sdk
import { plivo } from "@opencoredev/sms-sdk/plivo";

Environment variables

Variable Required Used for
PLIVO_AUTH_ID Yes Basic auth username, and part of the request URL.
PLIVO_AUTH_TOKEN Yes Basic auth password, and callback signature verification.
PLIVO_FROM One sender variable Default sender: a Plivo number, short code, or alphanumeric sender ID.
PLIVO_POWERPACK_UUID One sender variable Default sender: a Powerpack (number pool).

The examples and the doctor CLI use these names. The adapter itself reads no environment variables. Keep the token on the server.

Basic usage

import { createSmsClient } from "@opencoredev/sms-sdk";
import { plivo } from "@opencoredev/sms-sdk/plivo";

const sms = createSmsClient({
  adapters: [
    plivo({
      authId: process.env.PLIVO_AUTH_ID ?? "",
      authToken: process.env.PLIVO_AUTH_TOKEN ?? "",
      from: "+15550100001",
    }),
  ],
});

const result = await sms.send({ to: "+14155550123", body: "Your order has shipped." });
console.log(result.providerId); // the first message_uuid

plivo() throws ConfigurationError at startup when the Auth ID or Auth Token is empty.

Authentication

Plivo uses HTTP Basic auth with the Auth ID and Auth Token of the account or a subaccount. The same Auth Token verifies callbacks signed with X-Plivo-Signature-V2. Callbacks signed with X-Plivo-Signature-Ma-V2 use the main account’s token.

Configuration

Option Type Default Meaning
authId string required Auth ID.
authToken string required Auth Token.
from SmsFrom none Default sender. { messagingService: powerpackUuid } sends from a Powerpack.
baseUrl string https://api.plivo.com API origin, for a proxy or a mock server.
fetch FetchLike global fetch Custom fetch.

API version and endpoint

  • POST https://api.plivo.com/v1/Account/{auth_id}/Message/
  • HTTP Basic auth, JSON body.
  • Fields sent: src or powerpack_uuid, dst, text, type (sms or mms), media_urls, url with method: "POST" (from webhookUrl), message_expiry (from validityPeriodSec), and any provider options.
  • Success: any 2xx with a non-empty message_uuid array, because Plivo does not state the exact success status. A 2xx without message_uuid is an unknown outcome.

Senders

Sender Write Sent as
Long code or toll-free number "+15550100001" src
Short code { shortCode: "12345" } src
Alphanumeric sender ID { senderId: "Acme" } src
Powerpack { messagingService: "<powerpack uuid>" } powerpack_uuid

Which senders work depends on the destination country.

Regions and countries

Plivo documents these rules in US and Canada messaging:

  • US long codes need 10DLC registration, which can take up to a week.
  • Sending from unverified toll-free numbers is prohibited in both the US and Canada.
  • Short codes must be preapproved by the carriers, and provisioning can take 8 to 12 weeks.
  • Alphanumeric sender IDs can’t send to the US or Canada. Use an SMS-enabled Plivo number there.

In some other countries, alphanumeric sender IDs must be preregistered with a local carrier. Plivo’s sender ID usage page explains registration and links the list of country requirements.

SMS SDK does not check these rules before sending, and they have not been tested against live accounts. See Sender registration and consent.

Capabilities

Capability Plivo adapter
MMS Yes
Scheduling No
Validity period 5 to 10799 seconds
Per-message webhookUrl Yes
Delivery status webhooks Yes
Inbound webhooks Yes
Native idempotency No

Provider options

Pass Plivo’s other Message API parameters in providerOptions.plivo:

import { createSmsClient } from "@opencoredev/sms-sdk";
import { plivo } from "@opencoredev/sms-sdk/plivo";

const sms = createSmsClient({
  adapters: [
    plivo({
      authId: process.env.PLIVO_AUTH_ID ?? "",
      authToken: process.env.PLIVO_AUTH_TOKEN ?? "",
      from: "+15550100001",
    }),
  ],
});

await sms.send({
  to: "+14155550123",
  body: "Your code is 123456",
  providerOptions: { plivo: { trackable: true, log: "number_only" } },
});
Option Plivo parameter Type
log log "true" | "false" | "content_only" | "number_only". Plivo documents it as a string; default "true".
trackable trackable boolean, for messages with a trackable action such as a 2FA code. Default false.
extra any other parameter Record<string, string | number | boolean>, sent as is

Unknown keys, wrong types, and extra keys that name a field SMS SDK sets throw before any request. The options go out only when the plivo adapter sends. See providerOptions for fallback and idempotency.

SMS SDK sets src, powerpack_uuid, dst, text, type, media_urls, url, method, and message_expiry itself, so extra cannot. Plivo’s India DLT parameters are deprecated and have no typed option. template, interactive, and location are WhatsApp-only.

Example: send and track delivery

import { createSmsClient, isE164, isSmsError } from "@opencoredev/sms-sdk";
import { plivo } from "@opencoredev/sms-sdk/plivo";

const from = process.env.PLIVO_FROM;
const to = process.env.SMS_EXAMPLE_TO;
if (!isE164(from) || !isE164(to)) {
  throw new Error("Set PLIVO_FROM and SMS_EXAMPLE_TO to E.164 numbers.");
}

const sms = createSmsClient({
  adapters: [plivo({ authId: process.env.PLIVO_AUTH_ID ?? "", authToken: process.env.PLIVO_AUTH_TOKEN ?? "", from })],
});

try {
  const result = await sms.send({
    to,
    body: "Your order has shipped.",
    idempotencyKey: "order:123:shipped:v1",
    webhookUrl: "https://example.com/webhooks/sms/plivo",
  });
  console.log(`Plivo accepted ${result.providerId} (${result.delivery})`);
} catch (error) {
  if (isSmsError(error)) console.error(error.toJSON());
  throw error;
}

You see Plivo accepted <uuid> (queued). Acceptance is not delivery. Plivo posts status callbacks to the webhookUrl, covered in Delivery status webhooks.

Delivery and inbound webhooks

  • Status: pass webhookUrl per message. Plivo calls it with POST.
  • Inbound: link the number to a Plivo application and set the application’s message URL.
import { parseSmsWebhook, WebhookSignatureError } from "@opencoredev/sms-sdk/webhooks";
import { db } from "./db";

export async function POST(request: Request): Promise<Response> {
  try {
    const event = await parseSmsWebhook({
      provider: "plivo",
      request,
      publicUrl: "https://example.com/webhooks/sms/plivo",
      credentials: { authToken: process.env.PLIVO_AUTH_TOKEN ?? "" },
    });
    if (!(await db.webhookEvents.insertIfNew(event.dedupeKey))) {
      return new Response(null, { status: 200 }); // also blocks replays
    }
    if (event.type === "message.delivered" || event.type === "message.undelivered") {
      await db.messages.updateStatus(event.providerId, event.type, event.errorCode);
    } else if (event.type === "recipient.opted_out") {
      await db.suppressions.add(event.from);
    }
    return new Response(null, { status: 200 });
  } catch (error) {
    if (error instanceof WebhookSignatureError) return new Response("Invalid signature", { status: 401 });
    throw error;
  }
}

Status mapping: queued becomes message.queued; sent becomes message.sent; delivered becomes message.delivered; undelivered and failed become message.undelivered. Plivo documents no filter signal, so no Plivo event is message.filtered. read and other statuses become unrecognized. ErrorCode 000 means success and is left out of errorCode.

dedupeKey is plivo:{MessageUUID}:{Status} or plivo:{MessageUUID}:received. Plivo sends no opt-out flag, so STOP and HELP come from keyword detection.

Signature verification

Plivo signs message callbacks with X-Plivo-Signature-V2 or X-Plivo-Signature-Ma-V2, plus X-Plivo-Signature-V2-Nonce. The signature is a Base64 HMAC-SHA256, keyed with the Auth Token, over the callback URL without its query string followed by the nonce.

  • Pass publicUrl with the exact URL Plivo calls, or verification fails behind a proxy. See Webhook security.
  • The body is not signed and there is no timestamp. Serve the endpoint over HTTPS, and deduplicate on dedupeKey so a replayed callback has no effect.

Error mapping

Plivo response Category Falls back
401 auth Yes
429 with Plivo’s api_id in the body rate_limited Yes, after retries
429 without api_id unknown Never
Any other 4xx request No
5xx, network error, timeout, 2xx without message_uuid unknown Never

Plivo documents an api_id on every response. A 429 without one came from something else, such as a proxy, and proves nothing. Because Plivo’s error body is undocumented, SMS SDK never produces sender, account, recipient, or compliance, so an opt-out or a bad sender appears as request. To tell causes apart in logs, read Plivo’s error text in error.provider.message and its api_id in error.provider.requestId.

Limitations

  • Most rejections are request and never fall back. If you rely on fallback, put a supported adapter first, or accept that Plivo sender problems stop the send.
  • Opt-outs arrive as request, not compliance, so code that adds category === "compliance" rejections to a suppression list misses them.
  • No scheduling. Use Telnyx, or Twilio with a Messaging Service.
  • Callback bodies are not signed and have no replay window.
  • Plivo states a default message_expiry of 10800 seconds, outside its own documented range of 5 to 10799. SMS SDK enforces 5 to 10799.

Testing

Point plivo() at a fake API with mockFetch:

import { createSmsClient } from "@opencoredev/sms-sdk";
import { plivo } from "@opencoredev/sms-sdk/plivo";
import { mockFetch } from "@opencoredev/sms-sdk/testing";

const fake = mockFetch({
  status: 202,
  body: { message: "message(s) queued", message_uuid: ["db3ce55a-7f1d-11e1-8ea7-1231380bc196"], api_id: "db342550-7f1d-11e1-8ea7-1231380bc196" },
});
const sms = createSmsClient({
  adapters: [plivo({ authId: "MAXXXXXXXXXXXXXXXXXX", authToken: "test", from: "+15550100001", fetch: fake.fetch })],
});

const result = await sms.send({ to: "+14155550123", body: "Hi" });
console.log(result.providerId); // "db3ce55a-7f1d-11e1-8ea7-1231380bc196"

Test your callback handler with signedPlivoRequest(). See Local testing.

The adapter’s own tests live in the SMS SDK repository. From packages/sms-sdk:

bun test test/contracts/plivo.test.ts test/webhooks/plivo.test.ts

The live test sends one real, billable message to each configured provider. It is skipped unless you opt in:

LIVE_SMS_TESTS=true LIVE_SMS_TO=+14155550123 LIVE_SMS_ALLOWLIST=+14155550123 \
PLIVO_AUTH_ID=... PLIVO_AUTH_TOKEN=... PLIVO_FROM=+15550100001 \
bun test test/integration/live.test.ts

Check your configuration first with npx @opencoredev/sms-sdk doctor --adapter plivo.

API reference

plivo(options: PlivoOptions): SmsAdapter

type PlivoOptions = {
  authId: string;
  authToken: string;
  from?: SmsFrom;
  baseUrl?: string;
  fetch?: FetchLike;
};

Also exported from @opencoredev/sms-sdk/plivo: PLIVO_CAPABILITIES, PLIVO_SUPPORT_NOTES, PLIVO_PROVIDER_OPTIONS, buildPlivoRequest(message), interpretPlivoResponse(status, text, headers), plivoRejectionCategory(status), and the types PlivoOptions, PlivoMessageRequest, PlivoSendOptions.

Official documentation

Checked on 2026-10-08. The full evidence list is in PROVIDERS.md.

Next steps