---
title: "Plivo"
description: "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 verifies Plivo's V2-signed message callbacks. To compare providers, see [Choose a provider](/providers/overview).

## Support status

**Partial.** The adapter works and passes the shared contract tests. Gaps in Plivo's documentation limit it, and one feature is missing:

- Plivo does not document its API error body. SMS SDK can only recognize a 401 (`auth`) and a 429 that carries Plivo's `api_id` (`rate_limited`). Every other 4xx is a `request` rejection that never falls back, even when the real cause is a sender or account problem another provider could avoid.
- Plivo's message callback signature covers the URL and a nonce, but neither the body nor a timestamp. A captured callback can be replayed, or its body changed, and still pass verification. Plivo's V3 scheme, which signs parameters, is documented for Voice only.
- No scheduling.

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

## Setup

```package-install
npm install @opencoredev/sms-sdk
```

| 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 verification. |
| `PLIVO_FROM` or `PLIVO_POWERPACK_UUID` | One of them | Default sender: a Plivo number, short code, or alphanumeric sender ID, or a Powerpack. |

The adapter reads no environment variables itself; the examples and the [doctor CLI](/reference/doctor-cli) use these names. Keep the token on the server.

```ts
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
```

Use the Auth ID and Auth Token of the account or a subaccount. `plivo()` throws `ConfigurationError` at startup when either is empty. Two more options exist for tests and proxies: `baseUrl` (default `https://api.plivo.com`) and `fetch`. To test without a Plivo account, see [Test without a provider](/testing/local-testing).

The adapter calls `POST /v1/Account/{auth_id}/Message/` with a JSON body. Plivo doesn't state an exact success status, so any 2xx with a non-empty `message_uuid` array counts as accepted. A 2xx without one 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 (number pool) | `{ messagingService: "<powerpack uuid>" }` | `powerpack_uuid` |

Which senders work depends on the destination country. In the US, long codes need 10DLC registration, unverified toll-free numbers can't send to the US or Canada, and short codes need carrier approval (8 to 12 weeks). Alphanumeric sender IDs can't reach the US or Canada, and some other countries require them to be registered first. Plivo's [US and Canada messaging](https://www.plivo.com/docs/messaging/concepts/us-ca-messaging) and [sender ID usage](https://www.plivo.com/docs/messaging/concepts/sender-id-usage) pages cover the rules. SMS SDK does not check them before sending; see [Registration and consent](/providers/registration-and-consent).

## Features

| Feature | Plivo |
| --- | --- |
| MMS | Yes, sent with `type: "mms"` |
| Scheduling (`sendAt`) | No |
| Validity period | 5 to 10799 seconds, sent as `message_expiry` |
| Per-message `webhookUrl` | Yes, sent as `url` with `method: "POST"` |

Plivo states a default `message_expiry` of 10800 seconds, outside its own documented range. SMS SDK enforces 5 to 10799.

## Provider options

Pass other Plivo Message API parameters in `providerOptions.plivo`:

```ts
import { sms } from "./sms";

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 and wrong types throw before any request, and the options go out only when the `plivo` adapter sends. `extra` cannot set the fields SMS SDK owns: `src`, `powerpack_uuid`, `dst`, `text`, `type`, `media_urls`, `url`, `method`, and `message_expiry`. Plivo's India DLT parameters are deprecated and have no typed option, and `template`, `interactive`, and `location` are WhatsApp-only. See [`providerOptions`](/reference/send#provideroptions) for how options interact with fallback and idempotency.

## Webhooks

```ts
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;
  }
}
```

:::warning[Callbacks can be replayed]
Plivo signs neither the callback body nor a timestamp. Serve the endpoint over HTTPS and deduplicate on `dedupeKey`, as above, so a replayed callback has no effect.
:::

- **Credential:** the Auth Token. Callbacks signed with `X-Plivo-Signature-V2` use the account's or subaccount's token; callbacks signed with `X-Plivo-Signature-Ma-V2` use the main account's token.
- **Point Plivo at it:** for status, pass `webhookUrl` per message. For inbound, link the number to a Plivo application and set the application's message URL.
- **`publicUrl`:** pass the exact URL Plivo calls, or verification fails behind a proxy. See [Webhook security](/receiving/webhook-security).
- **Signing:** an HMAC-SHA256 over the callback URL without its query string, followed by `X-Plivo-Signature-V2-Nonce`.
- **Replay window:** none. Deduplication is your only replay protection.

| Plivo status | Event |
| --- | --- |
| `queued` | `message.queued` |
| `sent` | `message.sent` |
| `delivered` | `message.delivered` |
| `undelivered`, `failed` | `message.undelivered` |
| `read`, others | `unrecognized` |
| Inbound message | `message.received`, or a `recipient.*` event from STOP and HELP keywords (`source: "keyword"`) |

Plivo documents no filter signal, so no Plivo event is `message.filtered`, and it sends no opt-out flag, so opt-outs come from keyword detection. `ErrorCode` `000` means success and is left out of `errorCode`. `dedupeKey` is `plivo:{MessageUUID}:{Status}` or `plivo:{MessageUUID}:received`.

## Limitations

- Most rejections are `request` and never fall back. If you rely on fallback, put a fully supported adapter first, or accept that Plivo sender problems stop the send.
- Opt-outs arrive as `request`, not `compliance`, so code that adds `compliance` rejections to a suppression list misses them. Handle inbound STOP keywords instead.
- No scheduling. Use Telnyx, or Twilio with a Messaging Service.
- Callback bodies are not signed and have no replay window.

## Error codes

| 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, so a 429 without one came from something else, such as a proxy, and proves nothing. SMS SDK never produces `sender`, `account`, `recipient`, or `compliance` for Plivo. To tell causes apart in logs, read Plivo's `error` text in `error.provider.message` and its `api_id` in `error.provider.requestId`. For adapter authors, the subpath also exports `buildPlivoRequest`, `interpretPlivoResponse`, `plivoRejectionCategory`, and `PLIVO_SUPPORT_NOTES`.

## Official documentation

Checked on 2026-10-08. The full evidence list is in [PROVIDERS.md](https://github.com/opencoredev/sms-sdk/blob/main/packages/sms-sdk/PROVIDERS.md).

- [Send a message](https://www.plivo.com/docs/messaging/api/message/send-a-message)
- [Messaging API overview and status codes](https://www.plivo.com/docs/messaging/api/overview)
- [Message object and callbacks](https://www.plivo.com/docs/messaging/api/message)
- [Messaging signature validation](https://www.plivo.com/docs/messaging/concepts/signature-validation)
- [Official Node SDK signature code](https://github.com/plivo/plivo-node/blob/master/lib/utils/security.js)

## Next steps

- [Webhook security](/receiving/webhook-security)
- [Fallback and retries](/sending/fallback-and-retries)
- [STOP, HELP, and opt-outs](/receiving/opt-outs)
