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
- In the console open Numbers, then Import from another provider, and choose what you send: transactional, marketing, or both.
- Choose Telnyx, paste the API key, and select Scan account. The scan reads your messaging profiles and your phone numbers, read-only.
- Review the plan and deselect anything you do not want.
- Fill in the port authorization and confirm.
What the importer brings over
| In your account | Becomes in SimpleSMS |
|---|---|
| A phone number assigned to a messaging profile | A port request. A number with no messaging profile is treated as not SMS-enabled and skipped. |
| A messaging profile | A sender pool with the profile's name; member numbers join as their ports complete |
| The profile's webhook URL | One 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 number | Tags 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:
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."}'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 code | In SimpleSMS |
|---|---|
Authorization: Bearer KEY... | Authorization: Bearer ssms_sk_... (test and live keys are separate) |
text | body |
messaging_profile_id as the sender | A pool id (pool_...) as from |
webhook_url on the message or the profile | Webhook endpoints on the account |
message.finalized event | message.delivered or message.failed |
telnyx-signature-ed25519 and telnyx-timestamp headers | One 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:
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, orrejectedwith 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.