Migrate from Telnyx to SimpleSMS

Moving SMS to SimpleSMS is a short code change plus a move of your numbers, messaging profiles and webhook URLs, which the console importer prepares for you. This guide covers both.

What you need

  • A SimpleSMS account (signup issues a sandbox key immediately).
  • Your API key (it starts with KEY).
  • For porting: the name of the person authorized on the account and the account number. The importer cannot pre-fill the account number for this provider, so you enter it on the authorization step.

Run the importer

  1. In the console open Numbers, then Import from another provider, and choose what you send: transactional, marketing, or both.
  2. Choose Telnyx, paste the API key, and select Scan account. The scan reads your messaging profiles and your phone numbers, read-only.
  3. Review the plan and deselect anything you do not want.
  4. Fill in the port authorization and confirm.

What the importer brings over

In your accountBecomes in SimpleSMS
A phone number assigned to a messaging profileA port request. A number with no messaging profile is treated as not SMS-enabled and skipped.
A messaging profileA sender pool with the profile's name; member numbers join as their ports complete
The profile's webhook URLOne webhook endpoint subscribed to inbound events (message.received, message.opted_out, message.opted_in) and delivery events (message.sent, message.delivered, message.failed)
Tags on a numberTags on the SimpleSMS number, plus a from-telnyx tag

The importer proposes a single SimpleSMS endpoint per profile URL, carrying both inbound and delivery events. You can split them later in the console under Webhooks.

Change the code

The request stays a JSON POST with a bearer key. The URL changes and text becomes body:

bash
curl -X POST https://api.joinsimplesms.com/v1/messages \
  -H "Authorization: Bearer $SIMPLESMS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"from": "+15005550100", "to": "+15005550006", "body": "Your table is ready."}'
python
from joinsimplesms import SimpleSMS

client = SimpleSMS()  # reads SIMPLESMS_API_KEY

message = client.messages.send(
    to="+15005550006",
    body="Your table is ready.",
    from_="+15005550100",
)
In your current codeIn SimpleSMS
Authorization: Bearer KEY...Authorization: Bearer ssms_sk_... (test and live keys are separate)
textbody
messaging_profile_id as the senderA pool id (pool_...) as from
webhook_url on the message or the profileWebhook endpoints on the account
message.finalized eventmessage.delivered or message.failed
telnyx-signature-ed25519 and telnyx-timestamp headersOne simplesms-signature header: t=<timestamp>,v1=<HMAC-SHA256>

Two SimpleSMS behaviors to know before you cut over. A pool always uses the same member number for a given recipient, so a conversation stays on one number. And a key starting ssms_sk_test_ simulates delivery, with magic numbers that force each failure so you can test your error paths.

Rewrite the webhook handler

SimpleSMS signs with a shared secret (HMAC-SHA256 over the timestamp and the raw body), so the handler needs the endpoint's signing secret in place of a public key:

javascript
import { verifyWebhook } from 'joinsimplesms';

// Express: mount with express.raw({ type: 'application/json' }) so req.body is the raw bytes.
app.post('/webhooks/sms', async (req, res) => {
  let event;
  try {
    event = await verifyWebhook({
      payload: req.body,
      signature: req.headers['simplesms-signature'],
      secret: process.env.SIMPLESMS_WEBHOOK_SECRET,
    });
  } catch {
    return res.status(400).end();
  }

  if (event.type === 'message.received') {
    // event.data has message_id, from, to and body
  }
  if (event.type === 'message.delivered' || event.type === 'message.failed') {
    // event.data.message_id is the message you sent
  }
  res.status(200).end();
});

SimpleSMS posts one JSON event per delivery: { id, type, created_at, data }. Each endpoint has its own signing secret (whsec_...), shown next to the endpoint in the console. Respond with a 2xx within 5 seconds; a failed delivery is retried with backoff and can be replayed from the console (webhooks). message.delivered and message.failed are the final states of a message, and a failed message carries a readable failure object (errors).

After the import

  • Keep your current service running until each port completes. Running the plan creates things only on the SimpleSMS side. A number moves when the carrier completes its port, and each port shows its status in the console (requested, submitted, foc_set, complete, or rejected with the reason). See Number porting.
  • Register before you text the public. A US local number can reach people other than your verified recipients only once it is linked to an approved brand and campaign. Start the registration while the ports are in flight; carrier review usually takes 3 to 7 business days.
  • Bring your opt-outs. The importer does not copy a suppression list. Export yours and load it with POST /v1/consent/import (up to 500 numbers per call) or in the console, so nobody who said STOP hears from you again. From the first inbound message SimpleSMS enforces STOP, START and HELP itself (opt-out and consent).
  • Rehearse in the sandbox. A test key runs every endpoint with simulated delivery and real, signed webhooks, so the new handler can be finished before a single number has moved (sandbox).

What the importer does not move

  • Message history, contacts and templates. Contacts can be imported separately from a CSV (contacts).
  • Numbers outside the United States and Canada. They are listed and skipped.
  • Numbers that are not SMS-enabled. SimpleSMS numbers are for SMS only.
  • More than 2,000 numbers in one scan.

Your credentials are used for read-only requests during the scan and are not stored, logged or echoed back.

More in migrate