---
title: "Switch providers"
description: "Move SMS sending from one provider to another with SMS SDK: provision the sender, swap the adapter, move webhooks, run both during cutover, and keep a rollback path."
---

Switching providers is one adapter change in your code. Most of the work happens in the new provider's account, where you set up a sender, its registration, and webhooks. Do the steps below in order and you can roll back at any point. The examples move from Twilio to Telnyx; the same steps apply to any pair.

Still on the `twilio` npm package? Do [Migrate from the Twilio SDK](/migration/migrate-from-twilio) first.

## What changes

Your `sms.send()` calls, error handling, `validate()`, hooks, and webhook event handling stay the same. These change:

- The adapter import and its credentials.
- The sender. Numbers, short codes, 10DLC campaigns, and toll-free verifications belong to one provider account. SMS SDK does not move them or your message history.
- Webhook URLs and the credential that verifies them.
- Message IDs. New sends get the new provider's IDs. Old IDs keep matching late webhooks from the old provider.

Per provider:

| Provider | Adapter credentials | Webhook credential | Gaps to check |
| --- | --- | --- | --- |
| Twilio | `accountSid` plus `authToken`, or `apiKeySid` and `apiKeySecret` | Auth Token, plus `publicUrl` behind a proxy | `sendAt` needs a Messaging Service sender |
| Telnyx | `apiKey`; `messagingProfileId` for alphanumeric senders | Public key | No `validityPeriodSec` |
| Plivo | `authId`, `authToken` | Auth Token, plus `publicUrl` behind a proxy | No `sendAt`. Most rejections are `request`, so fallback rarely triggers. |
| Vonage | `applicationId`, `privateKey` (JWT), or `apiKey`, `apiSecret` (Basic) | Signature secret | No MMS, short codes, or `sendAt`. Basic auth gets no webhooks, so use JWT. |
| Bandwidth | `userId`, `apiToken`, `apiSecret`, `applicationId` | Callback username and password | No MMS, `sendAt`, `validityPeriodSec`, per-message `webhookUrl`, or alphanumeric senders |
| Sinch | `servicePlanId`, `apiToken` | Basic credentials or HMAC secret | No MMS. Set `providerOptions.sinch.deliveryReport` to get status webhooks. |

The [provider comparison](/providers/overview) has the full capability and sender tables.

## 1. Prepare the new provider

Using Telnyx as the example:

1. Create a Telnyx API v2 key.
2. Get a sender: buy a number, or port yours. A port moves the number off the old provider, so plan the cutover around it.
3. Assign the number to a messaging profile.
4. Register it: an A2P 10DLC brand and campaign for US long codes, or toll-free verification. Registrations do not transfer between providers. See [Registration and consent](/providers/registration-and-consent).
5. Copy the public key from Mission Control, under Keys & Credentials, for webhook verification.

Check the configuration without sending:

```bash
TELNYX_API_KEY=... TELNYX_FROM=+15550100002 npx @opencoredev/sms-sdk doctor --adapter telnyx
```

The [doctor CLI](/reference/doctor-cli) works for all six providers. It checks configuration format and reports registration as "not verified", because only the provider can confirm it.

## 2. Swap the adapter

```ts sms.ts
import { createSmsClient } from "@opencoredev/sms-sdk";
import { twilio } from "@opencoredev/sms-sdk/twilio"; // [!code --]
import { telnyx } from "@opencoredev/sms-sdk/telnyx"; // [!code ++]

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

A field the new provider does not support throws `UnsupportedFieldError` instead of being dropped. For Twilio to Telnyx:

- Remove `validityPeriodSec`. Telnyx has no such field.
- `sendAt` works from any Telnyx sender, not only a Messaging Service.
- A `{ messagingService: "MG…" }` sender becomes a Telnyx messaging profile ID or a number.
- Alphanumeric senders need `messagingProfileId` on the `telnyx()` adapter.

Run `sms.validate()` over a sample of real messages to catch these before you deploy.

## 3. Move webhooks

Add routes for the new provider next to the old ones. Keep the old routes running until its late status webhooks stop arriving. Both providers parse to `SmsEvent`, so one function handles both:

```ts webhooks.ts
import { parseSmsWebhook, WebhookSignatureError, type SmsEvent } from "@opencoredev/sms-sdk/webhooks";
import { db } from "./db";

async function handle(event: SmsEvent): Promise<void> {
  if (!(await db.webhookEvents.insertIfNew(event.dedupeKey))) return;
  if (event.type === "message.delivered" || event.type === "message.undelivered" || event.type === "message.filtered") {
    await db.messages.updateStatus(event.providerId, event.type, event.errorCode);
  } else if (event.type === "recipient.opted_out") {
    await db.suppressions.add(event.from);
  }
}

export async function telnyxWebhook(request: Request): Promise<Response> {
  try {
    await handle(await parseSmsWebhook({ provider: "telnyx", request, credentials: { publicKey: process.env.TELNYX_PUBLIC_KEY ?? "" } }));
    return new Response(null, { status: 200 });
  } catch (error) {
    if (error instanceof WebhookSignatureError) return new Response("Invalid signature", { status: 401 });
    throw error;
  }
}

export async function twilioWebhook(request: Request): Promise<Response> {
  try {
    await handle(
      await parseSmsWebhook({
        provider: "twilio",
        request,
        publicUrl: "https://example.com/webhooks/sms/twilio",
        credentials: { authToken: process.env.TWILIO_AUTH_TOKEN ?? "" },
      }),
    );
    return new Response("<Response></Response>", { headers: { "content-type": "text/xml" } });
  } catch (error) {
    if (error instanceof WebhookSignatureError) return new Response("Invalid signature", { status: 401 });
    throw error;
  }
}
```

In Telnyx, set the messaging profile's webhook URL to the new route. It receives both status and inbound events. Where to set the URL for each provider is in [Webhook events](/reference/webhook-events#where-to-point-webhooks). Dedupe keys start with the provider name, so keys from the two providers never collide.

## 4. Carry over opt-outs

The old provider, not the new one, holds the opt-outs from people who replied STOP to your old number. Before the first send on the new provider:

- Make sure your suppression list has every opt-out you received through webhooks.
- Check the list in `beforeSend` so it applies to every provider. See [STOP, HELP, and opt-outs](/receiving/opt-outs#block-sends-to-suppressed-recipients).
- If the provider kept its own list, such as Twilio Advanced Opt-Out, export it and import it into yours.

## 5. Run both during cutover

Put the new provider first and keep the old one as a safety net:

```ts sms.ts
import { createSmsClient } from "@opencoredev/sms-sdk";
import { telnyx } from "@opencoredev/sms-sdk/telnyx";
import { twilio } from "@opencoredev/sms-sdk/twilio";

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

A Telnyx `auth`, `rate_limited`, `sender`, or `account` rejection now goes through Twilio. Other rejections, and outcomes where Telnyx may have sent the message, never fall back. See [Fallback and retries](/sending/fallback-and-retries). Watch `result.attemptedProviders` or the `onAttempt` hook to see how often Twilio is used.

Each adapter sends from its own number, so recipients may see two sender numbers during this phase. If that matters, move one message type at a time instead, with a second client for the new provider.

## Rollback

Until you release the old number or close the account, rollback is a code change: put the old adapter back as the only or first adapter and redeploy. Keep the old webhook routes and credentials until you are sure.

## After the switch

- Remove the old adapter and credentials once no message uses it and its late webhooks have stopped.
- Keep old message IDs in your database. They never match the new provider's IDs, and they do not need to.
- Compare delivery rates per provider from your stored `provider` and status events.

## Next steps

- [Choose a provider](/providers/overview)
- [Fallback and retries](/sending/fallback-and-retries)
