Documentation
Sign inGet started

Migrate Verify from another provider

Use this guide to move phone and email one-time passcodes (OTP) from another verification provider to Bird Verify. The port is small, because the surface is small: two calls replace whatever your provider's create-and-check pair looks like, and Bird owns the code, the message, and the delivery channel behind them.
One structural difference sets the shape of the work. Bird has no per-application service object and no verification ID for you to track. A verification is identified by its recipient, so both calls take the same to, and the state your integration keeps shrinks to nothing.
The migration checklist:
  1. Map the create and check calls
  2. Set your channels, countries, and sender
  3. Port the verification lifecycle
  4. Switch webhooks
  5. Cut over one code lifetime at a time
Steps 1 and 3 depend on which provider you're leaving. Your provider guide has the field-by-field mapping and the status translation.

1. Map the create and check calls

POST /v1/verify/verifications sends a passcode. The smallest request is a recipient:
Exemplo de código
curl -X POST https://us1.platform.bird.com/v1/verify/verifications \
  -H "Authorization: Bearer $BIRD_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"to": {"phone_number": "+15551234567"}}'
POST /v1/verify/verifications/check submits what the user typed, keyed by the same recipient plus the code. The full payloads are in Sending verifications.
Four differences to handle while porting:
  • The recipient is the key. Providers that return a verification SID or ID expect it back on the check. Bird matches on the address set instead, and it must match exactly: a verification created with both an email and a phone number is not found by either one alone. Whatever column holds the provider's verification ID can be dropped.
  • A wrong code returns 200. The response carries success: false, a reason of incorrect_code, expired, or attempts_exhausted, and attempts_remaining. Reserve your error path for request failures. Once a verification reaches a final state, further checks return 404 rather than success: false.
  • Bird generates the code and never returns it. There is no custom-code parameter, so a provider integration that supplied its own passcode, or read the code back to send it itself, has no equivalent here.
  • Both endpoints take Idempotency-Key. A replay after a timeout returns the original response without sending another code or consuming an attempt.
Per-request options are deliberately few: options.code_length and options.channels, which reorders or narrows the channels for one request. Everything else is workspace configuration rather than a field on the send.

2. Set your channels, countries, and sender

Bird delivers codes over email, SMS, WhatsApp, and Telegram. For a phone recipient, most countries try WhatsApp first with SMS as the fallback, and delivery advances to the next channel in the plan when a send fails. Set the order, or turn a channel off, per country on the Countries page; disable the countries you do not serve while you are there, because an unused destination is exposure to SMS pumping rather than reach.
Two gaps are worth checking against your current flow before you commit to a date:
  • There is no voice-call channel and no silent network authentication. A flow that falls back to a phone call for users who cannot receive SMS needs a different answer here.
  • Bird owns the sender identity. Email, SMS, and WhatsApp codes are delivered under Authifly, Bird's verification brand, and Telegram uses its own verified notification account. You can point the email channel at a sender on your own verified domain; SMS and WhatsApp senders are Bird-managed. If your users are used to a code from your own sender ID, the message they see changes on cutover. Senders and branding shows each message as the recipient receives it.
Message content is Bird's too. Providers that let you pass a template ID, a locale, or a custom message body per request have no equivalent field.

3. Port the verification lifecycle

A verification is pending until it resolves: verified when a correct code arrives in time, failed with reason attempts_exhausted, or expired with reason ttl_elapsed. Map your provider's terminal states onto those three, and treat reason as an open enum.
The timings that shape your UI are workspace settings on the Configure page: how long a code stays valid, how many check attempts a user gets, and how long the resend cooldown runs. Set them to match what your users experience today rather than rewriting your UI copy. Code length is the one value you can also set per request. The defaults and the ranges are in Verification settings.
Two behaviors usually replace code you already have:
  • Resending is the create call again. Call create with the same recipient: inside the cooldown it returns the live verification without sending, and after it a fresh code goes out. Every code sent for a live verification stays valid until the verification resolves, so a user who enters the first one after the second arrives is not punished for it.
  • "I didn't get a code" has its own endpoint. POST /v1/verify/verifications/next-channel advances to the next channel in the plan and sends there immediately, ignoring the resend cooldown but keeping the expiry, attempt budget, and verification. Wire it to the button rather than looping resends on a channel that is not arriving.
On top of your settings sit platform guardrails you do not configure: a per-address hourly send cap and a per-recipient check cap, both answered with a 429 and a Retry-After. If your current provider let you raise per-endpoint rate limits and you did, check your peak against the figures in Abuse guardrails before cutover.

4. Switch webhooks

Verify emits events on two axes. Session events, verify.verification.created and verify.verification.verified, follow the verification itself. Attempt events, verify.attempt.sent, verify.attempt.delivered, and verify.attempt.undelivered, follow each individual passcode send, so a resend or a channel failover adds attempts to the same session. Subscribe an endpoint to the types you want with POST /v1/webhooks; the payloads are in Verify events.
These events serve analytics, alerting, and support tooling. Your authentication decision comes from the check call, which answers synchronously, and a login flow should never wait on a webhook to let a user in. Delivery is at-least-once and unordered, signed per Standard Webhooks, so deduplicate on the webhook-id header the same way as every other Bird event.

5. Cut over one code lifetime at a time

Verify has no simulated recipients: the thing worth testing is the code arriving, so run the integration against a phone number and a mailbox you control, on each channel you enabled, before touching production.
The cutover itself has one rule that is easy to miss. A code issued by your old provider cannot be checked by Bird, and the reverse. So switch at the create call, and for the length of one code lifetime, route each check to whichever provider issued that verification. In practice:
  1. Record which provider created each in-flight verification.
  2. Start sending a share of new verifications through Bird, and check those against Bird.
  3. Keep checking older verifications against the old provider until the last one expires, which takes one code validity window plus a margin.
  4. Raise Bird's share once conversion rates for the first cohort look right, then retire the old path.
Watch conversion rather than delivery alone. The Verifications page and the Verify metrics show sends, deliveries, and how many verifications reached verified, which is the number that tells you whether a channel order or a new sender identity is costing you sign-ups.

Migrating from a specific provider

  • Twilio Verify: Services become workspace settings, VerificationCheck becomes a recipient-keyed check, channel and status translation
  • Prelude: a near-identical create-and-check shape, with routing signals and silent verification as the parts that do not port

Next steps