# Migrate from Twilio to SimpleSMS

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

Moving SMS to SimpleSMS has two parts: the console importer, which reads your account and recreates numbers, sender pools and webhook endpoints, and a small code change. This guide covers both for messaging. For phone verification, see [Migrate from Twilio Verify](/docs/migrate-from-twilio).

## What you need

- A SimpleSMS account. A sandbox key is issued at [signup](/signup) and is enough to rehearse everything below.
- Your Account SID (starts with `AC`, 34 characters) and Auth Token (32 characters).
- For porting: the full name of the person authorized on the account, the account number, and the last 4 digits of the port-out PIN if you set one.

## 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 **Twilio**, paste the Account SID and Auth Token, and select **Scan account**. The scan makes read-only requests for your phone numbers and your Messaging Services with their sender numbers.
3. Review the plan. Nothing has been created yet, and you can deselect any line.
4. Fill in the port authorization and confirm. SimpleSMS creates the port requests, pools and webhook endpoints you left selected.

### What the importer brings over

| In your account | Becomes in SimpleSMS |
| --- | --- |
| A phone number with SMS capability | A port request, tagged `from-twilio` |
| A Messaging Service | A [sender pool](/docs/numbers) with the same name; its numbers join as their ports complete |
| A number's SMS webhook URL, or its Messaging Service's inbound URL when the service sets one | A webhook endpoint subscribed to `message.received`, `message.opted_out` and `message.opted_in` |
| A Messaging Service status callback URL | A webhook endpoint subscribed to `message.sent`, `message.delivered` and `message.failed` |
| A number's friendly name | The number's label |

Each number is marked as one of: will port, already on your SimpleSMS account, port already open, not SMS-enabled, or not a US or Canada number. If Messaging Services cannot be read with the credentials you gave, the numbers are still listed, without their services, and the plan says so.

## Change the code

Send with one JSON request and a bearer key:

```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 order has shipped."}'
```

```javascript
import { SimpleSMS } from 'joinsimplesms';

const sms = new SimpleSMS(process.env.SIMPLESMS_API_KEY);

const message = await sms.messages.send({
  from: '+15005550100',
  to: '+15005550006',
  body: 'Your order has shipped.',
});
```

| In your current code | In SimpleSMS |
| --- | --- |
| Account SID and Auth Token as HTTP Basic auth | One API key: `Authorization: Bearer ssms_sk_...` |
| Form-encoded `To`, `From`, `Body` | JSON `to`, `from`, `body` |
| `MessagingServiceSid` as the sender | A pool id (`pool_...`) as `from` |
| `StatusCallback` URL on each message | A webhook endpoint on the account, subscribed to delivery events |
| `X-Twilio-Signature` header on callbacks | `simplesms-signature` header: `t=<timestamp>,v1=<HMAC-SHA256>` |
| Form-encoded callback fields | One JSON event: `{ id, type, created_at, data }` |

The send returns the message with its `id`, `status`, `segments` and `price`. Retrying is safe: the SDK attaches an `Idempotency-Key` for you, and with plain HTTP you can send your own ([messages](/docs/messages)).

## Rewrite the webhook handler

The imported endpoints keep your existing URLs, so the handler behind each one has to accept SimpleSMS events. Verify the signature against the raw body, then switch on the event type:

```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)).

## 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

- [Migrate from Twilio Verify](/docs/migrate-from-twilio)
- [Messages](/docs/messages) and [webhooks](/docs/webhooks)
- [API reference](/docs/api)
- [All migration guides](/migrate)
