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’sapi_id, which israte_limited. Every other 4xx is arequestrejection 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-sdkpnpm add @opencoredev/sms-sdkyarn add @opencoredev/sms-sdkbun add @opencoredev/sms-sdknub add @opencoredev/sms-sdkaube add @opencoredev/sms-sdkimport { 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:
srcorpowerpack_uuid,dst,text,type(smsormms),media_urls,urlwithmethod: "POST"(fromwebhookUrl),message_expiry(fromvalidityPeriodSec), and any provider options. - Success: any 2xx with a non-empty
message_uuidarray, because Plivo does not state the exact success status. A 2xx withoutmessage_uuidis 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
webhookUrlper message. Plivo calls it withPOST. - 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
publicUrlwith 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
dedupeKeyso 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
requestand 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, notcompliance, so code that addscategory === "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_expiryof 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.
- Send a message
- Messaging API overview and status codes
- Message object and callbacks
- Messaging signature validation
- Official Node SDK signature code