# Migrate from Twilio Verify

Source: https://jointextkit.com/docs/migrate-from-twilio
Index: https://jointextkit.com/llms.txt

Two calls become two calls. The main differences: no Verify Service to create,
no phone number to buy, and you're billed only when a code actually verifies.

## Send a code

```js
// Twilio
await twilio.verify.v2.services(SERVICE_SID)
  .verifications.create({ to: phone, channel: 'sms' });

// TextKit
await fetch('https://api.jointextkit.com/v1/verify', {
  method: 'POST',
  headers: { Authorization: `Bearer ${process.env.TEXTKIT_API_KEY}`, 'Content-Type': 'application/json' },
  body: JSON.stringify({ to: phone }),
});
```

## Check a code

```js
// Twilio
const check = await twilio.verify.v2.services(SERVICE_SID)
  .verificationChecks.create({ to: phone, code });
if (check.status === 'approved') { /* ... */ }

// TextKit
const res = await fetch('https://api.jointextkit.com/v1/verify/check', {
  method: 'POST',
  headers: { Authorization: `Bearer ${process.env.TEXTKIT_API_KEY}`, 'Content-Type': 'application/json' },
  body: JSON.stringify({ to: phone, code }),
});
const { verified } = await res.json();
if (verified) { /* ... */ }
```

## In Python

```python
# Twilio
client.verify.v2.services(SERVICE_SID).verifications.create(to=phone, channel="sms")
check = client.verify.v2.services(SERVICE_SID).verification_checks.create(to=phone, code=code)
ok = check.status == "approved"

# TextKit: pip install jointextkit
from jointextkit import TextKit
sms = TextKit()  # reads TEXTKIT_API_KEY
sms.verify.send(to=phone)
ok = sms.verify.check(to=phone, code=code)["verified"]
```

## What maps to what

| Twilio | TextKit |
| --- | --- |
| `to` | `to` (or `phone`, both work) |
| Account SID + Auth Token | one API key |
| Verify Service SID | nothing; no service to create |
| A purchased phone number | nothing; we send from our own numbers |
| `check.status === 'approved'` | `verified === true` |
| `status: 'pending'` | `verified: false`, `status: 'pending'` |
| 404 on bad code | `200` with `verified: false` (a wrong code isn't an exception) |
| Fraud Guard | Shield, always on |

## Bring your opt-outs

If you also send messages (not only verification codes) from Twilio, export
the numbers that opted out there and import them before you send from
TextKit. Twilio does not provide its opt-out list through an API, so it is
a file you export and upload: one phone number per row, with or without a
header. Console → Numbers → Bring your numbers → **Bring your opt-outs**, or
`POST /v1/consent/import`. The console import of numbers, pools and
webhooks does not run until this is done, or until you confirm you have no
opt-outs to carry over. Details: [Bring your opt-outs](/docs/opt-out#bring-your-opt-outs).

## Things that get simpler

- **No Verify Service.** Delete the `VA...` SID from your config.
- **No number.** Delete the number provisioning step entirely.
- **Billing follows success.** We charge when `verified` comes back true,
  so pumping attacks and abandoned signups cost you nothing.
- **Attempt/expiry state is in the response.** `attempts_remaining` and
  `expires_in` let you render "2 tries left" and a countdown without tracking
  anything yourself.

## Things to watch

- **US and Canada only** right now. If you verify internationally, keep Twilio
  for those routes or talk to us.
- **The SDK is optional.** The plain `fetch` calls above are the whole
  integration; `npm install jointextkit` adds typed errors, retries, and the CLI.
- Sandbox has no resend cooldown so you can iterate; live enforces 60 seconds
  per number.
