# Migrate from Telnyx to SimpleSMS

Source: https://joinsimplesms.com/migrate/telnyx
Index: https://joinsimplesms.com/llms.txt

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](/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 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](/docs/numbers) 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`:

```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 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](/docs/sandbox) 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](/docs/webhooks)). `message.delivered` and `message.failed` are the final states of a message, and a failed message carries a readable `failure` object ([errors](/docs/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](/docs/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](/docs/compliance) 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](/docs/opt-out)).
- **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](/docs/sandbox)).

## What the importer does not move

- Message history, contacts and templates. Contacts can be imported separately from a CSV ([contacts](/docs/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.

## Related

- [Webhooks](/docs/webhooks)
- [Sandbox and test numbers](/docs/sandbox)
- [API reference](/docs/api)
- [All migration guides](/migrate)
