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

SMS, one SDK.

Send with Twilio, Telnyx, Plivo, or Vonage. Switch providers without rewriting your app.

Open source. Type-safe. Safe fallbacks. Zero runtime dependencies.

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

const sms = createSmsClient({
  adapters: [
    twilio({
      accountSid: process.env.TWILIO_ACCOUNT_SID ?? "",
      authToken: process.env.TWILIO_AUTH_TOKEN ?? "",
      from: "+15550100001", // a number on your Twilio account
    }),
  ],
});

const result = await sms.send({
  to: "+14155550123",
  body: "Your order has shipped.",
  idempotencyKey: "order:123:shipped:v1",
});

Know the segment count before you send

One emoji switches a message from GSM-7 to UCS-2 and cuts each segment from 160 units to 70.validate() works this out locally, with no provider call.

  1. Your order has shipped.

    Encoding
    GSM-7
    Units
    23
    Per segment
    160
    Segments
    1
  2. Your package is ready 📦

    Encoding
    UCS-2
    Units
    24
    Per segment
    70
    Segments
    1
  3. Your appointment with Dr. Lee is tomorrow at 9:30 AM at 22 Market St. Reply C to confirm or R to reschedule. Please arrive 10 minutes early to fill in the check-in forms.

    Encoding
    GSM-7
    Units
    170
    Per segment
    153
    Segments
    2

Counts follow the GSM 03.38 rules. They are estimates: your provider decides what it bills.

Switch Twilio to Telnyx

Swap the adapter. Every sms.send() call in your app stays the same.

Credentials, the sender number, its registration, and your webhook URLs belong to each provider account. Set those up on Telnyx first.

Read the switching guide
import { createSmsClient } from "@opencoredev/sms-sdk";
import { twilio } from "@opencoredev/sms-sdk/twilio"; 
import { telnyx } from "@opencoredev/sms-sdk/telnyx"; 

export const sms = createSmsClient({
  adapters: [
    twilio({ 
      accountSid: process.env.TWILIO_ACCOUNT_SID ?? "", 
      authToken: process.env.TWILIO_AUTH_TOKEN ?? "", 
      from: "+15550100001", 
    }), 
    telnyx({ 
      apiKey: process.env.TELNYX_API_KEY ?? "", 
      from: "+15550100002", 
    }), 
  ],
});

// Unchanged
await sms.send({
  to: "+14155550123",
  body: "Your order has shipped.",
  idempotencyKey: "order:123:shipped:v1",
});

What it does

Provider portability

Twilio, Telnyx, Plivo, and Vonage sit behind one typed send call. Changing providers means changing the adapter, not your app.

adapters: [telnyx({ apiKey, from })]

Safe handoff and failover

Fallback is off by default. Turned on, it moves to the next provider only after a known rejection, never after an unknown outcome.

fallback: "on-known-rejection"

Segment preview

validate() checks the number, picks GSM-7 or UCS-2, and estimates segments locally, without calling a provider.

sms.validate({ to, body }).segments

Normalized webhooks

Delivery receipts and inbound messages arrive as one event shape, only after the provider signature checks out.

import { parseSmsWebhook } from "@opencoredev/sms-sdk/webhooks"

Providers

All four adapters pass the same contract tests. Supported means requests, errors, and webhooks follow the provider's official documentation. Partial means part of the provider's behavior is undocumented, and the SDK treats that part conservatively.

ProviderSendDelivery statusInboundSignature check
TwilioSupportedSupportedSupportedSupported
TelnyxSupportedSupportedSupportedSupported
PlivoPartialSupportedSupportedPartial
VonagePartialPartialPartialPartial
  • Plivo: Plivo does not document its error body, so only auth and rate-limit rejections are classified. Its callback signature covers the URL and a nonce, not the body or a timestamp.
  • Vonage: Error classification is inferred because Vonage does not document the HTTP status per error code. Webhooks need JWT auth. No short codes or MMS.

No dependencies. Test without a carrier.

The package has no runtime or peer dependencies. Adapters call provider HTTP APIs withfetch, and each one is its own import, so you only load the providers you use.

The memory() adapter records sends in process. Use it in unit tests and CI, so no real message goes out.

Local testing guide
import { createSmsClient } from "@opencoredev/sms-sdk";
import { memory } from "@opencoredev/sms-sdk/testing";

const sms = createSmsClient({ adapters: [memory()] });

await sms.send({ to: "+14155550123", body: "Test message" });
// No network calls. The adapter records every send.

Limits and questions

Does switching providers move my phone number?
No. A number, short code, or sender ID belongs to a provider account, and so does its 10DLC or toll-free registration. Moving it is a port or a new registration you do with the providers. SMS SDK only changes the code that talks to them.
Will it ever send the same message twice?
It avoids it, but no SMS library can promise exactly once. If a provider times out after it may have accepted the message, SMS SDK reports an unknown outcome and does not retry or fall back. Use idempotency keys and a shared store when you run more than one instance.
Are segment counts exact prices?
No. Segment counts are estimates from the encoding rules. Providers and carriers decide what they bill, and pricing changes by country and sender type.
Is this a chat or OTP product?
No. SMS SDK sends and receives transactional SMS. It has no conversation threads, inbox, OTP verification service, WhatsApp, RCS, or hosted API.

Start with one provider

Install the package, add an adapter, and send your first message.