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

Vonage

Send and receive SMS through the Vonage Messages API with the SMS SDK vonage() adapter: JWT vs Basic auth, senders, provider options, signed webhooks, and error mapping.

The vonage() adapter sends SMS through the Vonage Messages API and verifies its signed status and inbound webhooks. To compare it with other providers, see Choose a provider.

Support status

Partial. The adapter passes the shared contract tests. Gaps in Vonage’s documentation limit it:

  • It uses only the Messages API (POST /v1/messages, channel sms). The older SMS API at rest.nexmo.com/sms/json is not supported, and its webhooks do not verify with this parser.
  • Vonage does not document which HTTP status each error code uses. SMS SDK classifies rejections from the documented 401, 402, and 422 statuses and from the error code in the response. Unknown codes become request, which never falls back.
  • Vonage describes the webhook payload_hash only as “a SHA-256 hash of the payload”. SMS SDK checks it against the exact raw body. If Vonage ever hashes a re-serialized body instead, verification fails with body_hash_mismatch.
  • Vonage documents no freshness window for the webhook JWT. SMS SDK accepts tokens issued within 300 seconds.
  • No short codes and no MMS.

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

Setup

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
Variable Required Used for
VONAGE_APPLICATION_ID, VONAGE_PRIVATE_KEY For JWT auth Application ID and its RSA private key as PKCS#8 PEM. Literal \n sequences are accepted.
VONAGE_API_KEY, VONAGE_API_SECRET For Basic auth Account API key and secret.
VONAGE_FROM No Default sender: a Vonage number or an alphanumeric sender ID.
VONAGE_SIGNATURE_SECRET For webhooks Signature secret from the Vonage dashboard settings.

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

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

const sms = createSmsClient({
  adapters: [
    vonage({
      applicationId: process.env.VONAGE_APPLICATION_ID ?? "",
      privateKey: process.env.VONAGE_PRIVATE_KEY ?? "",
      from: "+15550100001",
      region: "api-eu", // optional: "api" (default), "api-eu", "api-us", "api-ap"
    }),
  ],
});

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

vonage() throws ConfigurationError at startup when credentials are empty or the private key is not a PEM. A PEM that is not an RSA PKCS#8 key makes each send fail with category auth. Each request gets a fresh RS256 token, valid for 15 minutes. baseUrl overrides region; fetch is optional.

Pick an auth mode:

JWT (applicationId, privateKey) Basic (apiKey, apiSecret)
Status and inbound webhooks Yes No
Per-message webhookUrl Yes No, throws UnsupportedFieldError
Numbers linked to an application Work Fail with 401

Use JWT auth for anything beyond fire-and-forget sends. With Basic auth, adapter.capabilities reports webhookUrlOverride, inbound, and deliveryReceipts as false.

Senders

Sender Write it as Supported
Long code or toll-free number "+15550100001" Yes. Sent without the +, as Vonage requires.
Alphanumeric sender ID { senderId: "Acme" } Yes, where the destination country allows it
Short code No
{ messagingService } No

In the US, long codes need 10DLC registration, toll-free numbers must be verified (they then cover the US and Canada), and alphanumeric sender IDs are not allowed. Some other countries require registration through Vonage’s Global Sender ID portal; Vonage lists each country’s rules. SMS SDK does not check these rules and they have not been tested against live accounts. See Registration and consent.

Features

Feature Support
MMS (mediaUrls) No
Scheduling (sendAt) No
Validity period (validityPeriodSec) 20 to 604,800 seconds, sent as ttl
Per-message webhookUrl JWT auth only, sent as webhook_url
Empty body Not allowed
Native idempotency No. Use an idempotency store.

Provider options

Pass other Messages API SMS fields in providerOptions.vonage:

// Loading the adapter module adds the typed providerOptions.vonage entry.
import "@opencoredev/sms-sdk/vonage";
import { sms } from "./sms";

await sms.send({
  to: "+14155550123",
  body: "Your order has shipped.",
  providerOptions: { vonage: { clientRef: "order-123", encodingType: "text" } },
});
Option Wire field Type
clientRef client_ref string, up to 100 characters, returned in every status webhook
webhookVersion webhook_version "v0.1" | "v1". parseSmsWebhook reads v1.
trustedRecipient trusted_recipient boolean. Skips Fraud Defender protections (Fraud Defender Premium only).
encodingType sms.encoding_type "text" | "unicode" | "auto"
contentId sms.content_id string, a regulatory ID some countries require
entityId sms.entity_id string, a regulatory ID some countries require
poolId sms.pool_id string, a Number Pool to send from. from is still sent and used if the pool cannot be.
extra any other field Record<string, string | number | boolean>, sent as is. A key like sms.new_field goes inside the sms object.

Unknown keys and wrong types throw before any request. extra cannot set a field SMS SDK sets (message_type, channel, to, from, text, ttl, webhook_url, sms), nor failover, which would send further messages SMS SDK cannot track. Use sms.<name> keys instead of sms. Use trusted_recipient, not Vonage’s deprecated trusted_sender. See providerOptions for fallback behavior.

Webhooks

Vonage signs each webhook with an HS256 JWT in the Authorization header, using your signature secret. Pass that secret to the parser:

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: "vonage",
      request,
      credentials: { signatureSecret: process.env.VONAGE_SIGNATURE_SECRET ?? "" },
    });
    if (!(await db.webhookEvents.insertIfNew(event.dedupeKey))) return new Response(null, { status: 200 });
    if (event.type === "message.delivered" || event.type === "message.undelivered") {
      await db.messages.updateStatus(event.providerId, event.type, event.errorCode);
    } else if (event.type === "message.received") {
      await db.inbox.save({ from: event.from, to: event.to, body: event.body, providerId: event.providerId });
    }
    return new Response(null, { status: 200 });
  } catch (error) {
    if (error instanceof WebhookSignatureError) return new Response("Invalid signature", { status: 401 });
    throw error;
  }
}

To point Vonage at your handler, enable signed webhooks on the Vonage application, then:

  • Status: pass webhookUrl per message, or set the Status URL on the application.
  • Inbound: link the number to the application and set its Inbound URL.

The parser accepts only HS256, rejects a token whose iat is more than 300 seconds from now (toleranceSec changes this), and compares any payload_hash with the SHA-256 of the exact raw body, so pass the original request. The URL is not signed, so proxies do not break verification. In tests, signedVonageRequest() from @opencoredev/sms-sdk/testing builds signed requests.

Vonage status SMS SDK event
inbound message message.received, or recipient.opted_out / recipient.help for STOP and HELP keywords
submitted message.sent
delivered message.delivered
rejected, undeliverable with error 1210, 1470, 1472, or 1480 to 1483 message.filtered
rejected, undeliverable with any other error message.undelivered
read and anything else unrecognized

Vonage sends no queued status. It sends numbers without +; SMS SDK restores it, so from and to are E.164. dedupeKey is vonage:{message_uuid}:{status} or vonage:{message_uuid}:received. STOP and HELP come from keyword detection on the text; Vonage’s sms.keyword field is not read.

Limitations

  • No MMS and no short codes. Use Twilio, Telnyx, or Plivo.
  • No scheduling. Use Telnyx, Sinch, or Twilio with a Messaging Service.
  • Basic auth has no webhooks and fails for numbers linked to an application.
  • Error classification is inferred, so some sender or account problems may arrive as request and not fall back.

Error codes

SMS SDK reads the error code from the fragment of the problem type URL (…#1420, or …#throttled from Vonage’s generic error list) or from a numeric title. A listed code wins over the HTTP status.

Vonage response Category Falls back
Error 1000, 1241, or throttled rate_limited Yes, after retries
Error 1120 or 1420 sender Yes
402, or error 1060, 1080, 1160, 1290, or 1460 account Yes
401 or 403 without a listed code auth Yes
Error 1170 or 1430 recipient No
Error 1240 or 1476 compliance Never
422 and any other 4xx request No
429 without a throttling code unknown Never
5xx, network error, timeout, or 2xx without message_uuid unknown Never

A 429 without a documented throttling code may come from a proxy, so it proves nothing and the outcome is unknown. error.provider.requestId holds the X-Request-Id header. See Fallback and retries for what each category does.

Official documentation

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

Next steps