# Migrate Verify from Twilio

This page maps Twilio Verify v2 to Bird Verify. Follow the [main migration guide](/docs/guides/verify/migrate) in order and use these mappings for steps 1 and 3.

The Service is the piece with no counterpart. Twilio addresses `POST https://verify.twilio.com/v2/Services/{ServiceSid}/Verifications`, and the Service holds code length, TTL, lookup, landline handling, and rate limits. Bird addresses [`POST /v1/verify/verifications`](/docs/api/reference/create-verification) with no service segment: those settings are workspace configuration. If you ran several Services to separate brands, apps, or environments, that separation becomes separate [workspaces](/docs/guides/workspaces), each with its own API key and settings, rather than an ID in the path.

## Map the create call

| What it does      | Twilio Verify                                               | Bird                                                    |
| ----------------- | ----------------------------------------------------------- | ------------------------------------------------------- |
| Recipient         | `To`                                                        | `to.phone_number` or `to.email`                         |
| Channel           | `Channel`                                                   | `options.channels`, else the country's configured order |
| Code length       | Service `CodeLength`                                        | `options.code_length`, else the workspace default       |
| Code lifetime     | Service TTL                                                 | the workspace **Duration** setting                      |
| Attempt limit     | Service max attempts                                        | the workspace **Maximum Retries** setting               |
| Correlation       | `Tags`                                                      | `metadata`                                              |
| Safe retries      | (none)                                                      | `Idempotency-Key` header                                |
| Custom passcode   | `CustomCode`                                                | no equivalent                                           |
| Localization      | `Locale`                                                    | no equivalent                                           |
| Message content   | `TemplateSid`, `CustomFriendlyName`, `ChannelConfiguration` | no equivalent, apart from a branded email sender        |
| Per-key throttles | `RateLimits`                                                | fixed platform guardrails                               |
| Fraud controls    | `RiskCheck`, Fraud Guard, `DeviceIp`                        | not exposed on the API                                  |
| SMS autofill      | `AppHash`                                                   | no equivalent                                           |
| PSD2              | `Amount`, `Payee`                                           | no equivalent                                           |

The channels do not line up one to one either:

| Twilio `Channel` | Bird                                                       |
| ---------------- | ---------------------------------------------------------- |
| `sms`            | `sms`                                                      |
| `email`          | `email`                                                    |
| `whatsapp`       | `whatsapp`                                                 |
| `call`           | no equivalent                                              |
| `sna`, `auto`    | no equivalent                                              |
| `rcs`            | no equivalent                                              |
| (none)           | `telegram`, available for numbers registered with Telegram |

A flow that uses `call` as the accessibility fallback, or `sna` and `auto` for a codeless path, needs rethinking before you commit to a date. Everything else is a channel-order change on the [**Countries**](https://bird.com/dashboard/w/verify/countries) page rather than a per-request parameter.

## Map the check call

Twilio's `POST /v2/Services/{ServiceSid}/VerificationCheck` takes `To` **or** `VerificationSid`, plus `Code`. Bird's [`POST /v1/verify/verifications/check`](/docs/api/reference/create-verification-check) takes only the recipient and the code, so the `VerificationSid` path disappears along with the column you stored it in. Supply exactly the address set you created the verification with.

The result shape differs where it matters most:

- **Twilio answers with a `status` field; Bird answers with a boolean.** `success: true` means verified. `success: false` carries a `reason` of `incorrect_code`, `expired`, or `attempts_exhausted`, plus `attempts_remaining`, so the "how many tries left" figure you may be counting yourself comes back on the response.
- **Both go to `404` once the verification is spent.** Twilio deletes the verification when it is approved, expired, or out of attempts; Bird stops accepting checks in any final state. Store the first definitive answer rather than re-checking.

## Translate statuses

| Twilio status          | Bird status                                      | Bird reason          |
| ---------------------- | ------------------------------------------------ | -------------------- |
| `pending`              | `pending`                                        | none                 |
| `approved`             | `verified`                                       | none                 |
| `max_attempts_reached` | `failed`                                         | `attempts_exhausted` |
| `expired`              | `expired`                                        | `ttl_elapsed`        |
| `canceled`             | no equivalent: a verification is not cancellable |                      |

There is no update endpoint, so the Twilio pattern of forcing a verification to `approved` or `canceled` from your backend has no counterpart. A verification ends when the user verifies it, exhausts the attempts, or lets it expire.

## Move the event stream

Twilio Verify reports activity through Event Streams: a sink plus a subscription to verification status events, configured outside the Verify API. Bird uses the same webhook mechanism as every other channel. Subscribe an endpoint to `verify.verification.*` for session events and `verify.attempt.*` for individual passcode deliveries, and verify the signature per [Standard Webhooks](https://www.standardwebhooks.com). See [Verify events](/docs/guides/verify/events).

The two axes matter when you port dashboards. Twilio's verification status events line up with Bird's session events, and Bird's attempt events add per-send delivery outcomes on the same session, including the sends that a resend or a channel failover produces.

## Cut over

The [cutover rule](/docs/guides/verify/migrate#5-cut-over-one-code-lifetime-at-a-time) in the main guide is the one to plan around: a code issued by Twilio cannot be checked by Bird, so switch at the create call and keep routing checks to whichever provider issued the verification until the last Twilio code expires.

Also budget for the sender change. Twilio Verify delivers under your Service's friendly name and your own sender pool; Bird delivers under Authifly on email, SMS, and WhatsApp, with your own domain available for the email channel. Users mid-signup see a different sender, and support scripts that say "look for a text from us" need updating.

## Next steps

- [Sending verifications](/docs/guides/verify/sending-verifications): the full contract for both calls, statuses, and limits
- [Country configuration](/docs/guides/verify/countries): where channel order and availability now live
- [Senders and branding](/docs/guides/verify/senders): what the recipient sees on each channel
- [Verify events](/docs/guides/verify/events): the events your Event Streams consumer moves to