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 })]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",
});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.
Your order has shipped.
Your package is ready 📦
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.
Counts follow the GSM 03.38 rules. They are estimates: your provider decides what it bills.
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 guideimport { 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",
});Twilio, Telnyx, Plivo, and Vonage sit behind one typed send call. Changing providers means changing the adapter, not your app.
adapters: [telnyx({ apiKey, from })]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"validate() checks the number, picks GSM-7 or UCS-2, and estimates segments locally, without calling a provider.
sms.validate({ to, body }).segmentsDelivery receipts and inbound messages arrive as one event shape, only after the provider signature checks out.
import { parseSmsWebhook } from "@opencoredev/sms-sdk/webhooks"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.
| Provider | Send | Delivery status | Inbound | Signature check |
|---|---|---|---|---|
| Twilio | Supported | Supported | Supported | Supported |
| Telnyx | Supported | Supported | Supported | Supported |
| Plivo | Partial | Supported | Supported | Partial |
| Vonage | Partial | Partial | Partial | Partial |
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.
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.Install the package, add an adapter, and send your first message.