# 从 Prelude 迁移 Verify

本页将 Prelude v2 验证 API 映射到 Bird Verify。按顺序参照[主迁移指南](/docs/guides/verify/migrate)，在步骤 1 和 3 中使用这些映射。

两者结构接近。Prelude 的 `POST https://api.prelude.dev/v2/verification` 和 `POST /v2/verification/check` 是一对使用 Bearer 认证、以目标而非验证 ID 为键的 create-and-check 接口，[`POST /v1/verify/verifications`](/docs/api/reference/create-verification) 和 [`POST /v1/verify/verifications/check`](/docs/api/reference/create-verification-check) 也是如此。对同一活跃接收方再次调用 create 会重试而非在两个平台上发起新的验证。无法移植的是风控层：Prelude 的信号、路由判定和静默验证在 Bird Verify API 中没有对应功能。

## 交给你的 Agent

将此内容粘贴到 Claude Code、Cursor 或 Codex 中。Agent 会根据你的代码仓库逐步执行本页内容，使用它已有的任何 Bird 接口：如果已连接 MCP 服务器就用它，如果已安装并登录 CLI 就用它。

```text
I am moving a phone verification integration from Prelude to Bird Verify. Route through it with me.
1. Check what you already have before setting anything up. If Bird's MCP server is connected, use its tools. If the Bird CLI is installed and signed in, use that. Either one is enough, and every step below is an action you take with whichever you have. Only if neither is present, follow https://bird.com/docs/ai/set-up-your-agent.md to set one up and sign me in. Every Bird docs page serves Markdown at its own URL with `.md` appended, so fetch that rather than the HTML.
2. Read https://bird.com/docs/guides/verify/migrate/prelude.md for the create, check and status mapping, and https://bird.com/docs/guides/verify/migrate.md for the order the steps go in.
3. Find and list my Prelude usage in this repository before you change anything: the /v2/verification and /v2/verification/check call sites, anywhere I read signals or a routing verdict, and anywhere I use dispatch_id for retry safety.
4. Tell me early which of these I depend on. Bird Verify has no voice channel and no silent or network-based authentication. It generates the code itself and never returns it, so I cannot supply my own. It accepts `options.language` but no per-request template or message body. Bird can use an existing SMS Sender ID or a connected WhatsApp number with an approved authentication template, configured per channel or country rather than per request; tell me whether my current sender can be kept. Prelude's risk layer does not port either: its signals, its routing verdicts and its silent verification have no counterpart in Bird Verify, so tell me every decision my code makes on those.
5. Configure my channels and destinations following https://bird.com/docs/guides/verify/countries.md and https://bird.com/docs/guides/verify/senders.md. While you are there, disable every country I do not actually verify into. An enabled destination I never send to is not reach, it is exposure to SMS pumping, so ask me which countries I serve rather than leaving the defaults.
6. Port the create and check calls using the mapping tables on the provider page, and move my status handling to Bird's events: https://bird.com/docs/guides/verify/sending-verifications.md and https://bird.com/docs/guides/verify/events.md.
7. Cut over at the create call, not all at once, because a code issued by Prelude cannot be checked by Bird and a code issued by Bird cannot be checked by Prelude. From the moment I say go, send every NEW verification to Bird, and keep routing each check to whichever provider issued that verification. Keep both paths live for one full code lifetime plus margin, then retire the old one. Tell me how you will decide which provider issued a given verification before you write any of it.
8. Test before any real traffic. Bird Verify has no simulated recipients, so do not look for a sandbox: the thing worth testing is the code arriving. Run the integration against a phone number and a mailbox I control, on each channel I enabled, and show me what arrived on each one.
9. Stop and ask me wherever a step needs a decision. Do not start routing new verifications to Bird until I have seen those test results and replied with the words cut over to Bird. Retiring the Prelude path is a separate step: ask me again and wait for me to reply with the words retire the Prelude path, and do not retire it while any code it issued could still be checked. A reply that agrees without naming what it is authorising is not authorisation. Finish by telling me what is left that only a person can do.
```

## 映射 create 调用

| 功能             | Prelude                                                                          | Bird                                                                       |
| ---------------- | -------------------------------------------------------------------------------- | -------------------------------------------------------------------------- |
| 接收方           | `target.type` + `target.value`                                                   | `to.phone_number` 或 `to.email`                                            |
| 验证码长度       | `options.code_size`                                                              | `options.code_length`                                                      |
| 渠道偏好         | `options.preferred_channel`、`options.channels`                                  | `options.channels`，否则使用该国家/地区配置的顺序                          |
| 关联             | `metadata.correlation_id`                                                        | `metadata`                                                                 |
| 投递回调         | `options.callback_url`                                                           | 订阅了您指定的 verify 事件类型的工作区 webhook                             |
| 自定义验证码     | `options.custom_code`                                                            | 无对应功能                                                                 |
| 本地化           | `options.locale`                                                                 | `options.language`                                                         |
| 发送方身份       | `options.sender_id`                                                              | 按渠道或国家/地区选择 Bird 托管或工作区自有的发送方，不支持按请求指定      |
| 消息模板         | `options.template_id`、`options.variables`                                       | 无按请求指定的对应功能；在 Verify 配置中选择已审批的 WhatsApp 身份验证模板 |
| Android 自动填充 | `options.app_realm`                                                              | 无对应功能                                                                 |
| 风控信号         | `signals`（IP、设备、指纹）                                                      | 不接受                                                                     |
| 安全重试         | 其 create 或 check 参考文档中无幂等键或请求头                                    | `Idempotency-Key` 请求头                                                   |
| 信号关联         | `dispatch_id`、"the identifier of the dispatch that came from the front-end SDK" | 无对应功能：Bird 不接受信号数据                                            |
| 回退控制         | `options.max_auto_fallbacks`、`options.force_challenge`                          | 该国家/地区的渠道计划                                                      |

`dispatch_id` 不是重试机制，不应与 `Idempotency-Key` 并列。Prelude 自己的参考文档将其定义为 "the identifier of the dispatch that came from the front-end SDK"：其 Signals SDK 从 `dispatchSignals()` 返回该值，你在 create 时转发它，以便其反欺诈层将捕获的浏览器信号与该验证匹配。其 create 和 check 参考文档记录了完整的请求集，没有幂等键，也没有自定义请求头，因此重试 create 对你来说并不安全。在 Bird 上，[`Idempotency-Key`](/docs/guides/idempotency) 请求头负责此功能。

渠道集合仅部分重叠。Bird 通过 email、SMS、WhatsApp 和 Telegram 投递；Prelude 的 RCS、Viber、Zalo、语音和静默渠道目前没有 Bird 对应功能。Prelude 通过 Viber 或 Zalo 触达的号码在这里回退到 SMS，这是一个值得在试点阶段测量而非在全量时才发现的投递率问题。

## 映射 check 调用

两个 check 端点都接收接收方和验证码，不需要验证 ID，因此此调用几乎可以原样移植。区别在于响应：

| Prelude `status`       | Bird                                              |
| ---------------------- | ------------------------------------------------- |
| `success`              | `success: true`                                   |
| `failure`              | `success: false`、`reason: incorrect_code`        |
| `expired_or_not_found` | `success: false`、`reason: expired`，或一个 `404` |
| （无直接对应值）       | `success: false`、`reason: attempts_exhausted`    |

Prelude 将 "wrong code" 和 "out of attempts" 合并为 `failure`；Bird 将它们分开，并同时返回 `attempts_remaining`，以便你向用户显示剩余尝试次数。已解决的验证返回 `404` 而非状态，因此应存储第一个确定性结果，而不是重复检查。

## 风控层的去向

Prelude 的 create 响应会报告路由判定：`status` 为 `success`、`retry`、`challenged`、`blocked` 或 `shadow_blocked`，拒绝时附带 `reason` 和 `risk_factors`，并通过 `method` 指明其选择的渠道。Bird 的 create 响应就是验证本身。没有可供分支的判定、没有可发送的信号对象、也没有影子拦截的等价功能，因此依赖 Prelude 判定来拦截注册的集成需要在调用 Bird 之前自行决策。

Bird 在该领域提供的能力更窄，主要是配置层面的：按国家启用以关闭你从不服务的目的地、[滥用防护](/docs/guides/verify/sending-verifications#abuse-guardrails)中描述的平台发送和检查上限，以及渠道计划本身。如果防刷保护是你选择 Prelude 的原因，请在安排迁移前评估这一差距。

## 迁移回调

Prelude 将投递状态发送到你为每个验证设置的 `callback_url`。Bird 投递到你的工作区注册的端点，每个端点订阅其需要的事件类型，因此 URL 不在请求体中。指定你的处理程序需要的事件类型：`verify.verification.created`、`verify.verification.verified` 和 `verify.verification.failed` 用于会话，`verify.attempt.sent`、`verify.attempt.delivered` 和 `verify.attempt.undelivered` 用于每次验证码发送。没有通配符可以代替它们。按照 [Standard Webhooks](https://www.standardwebhooks.com) 验证签名。载荷格式见 [Verify events](/docs/guides/verify/events)。

## 切换

主指南中的[切换规则](/docs/guides/verify/migrate#5-cut-over-one-code-lifetime-at-a-time)同样适用：Prelude 签发的验证码无法由 Bird 检查，因此在 create 调用处切换，并将每个 check 路由到签发该验证的提供方，直到最后一个验证过期。由于两个 API 都以接收方为键，分支只需在现有调用点周围加一个条件判断，而非重写。

在试点期间同时观察转化率和投递情况。Prelude 按请求在更广泛的渠道集合中路由；Bird 按你为每个国家设置的渠道顺序路由。如果某个市场的转化率下降，先调整该国家的渠道顺序，再对迁移下结论。

## 后续步骤

- [发送验证](/docs/guides/verify/sending-verifications)：两个调用、状态和限制的完整契约
- [国家配置](/docs/guides/verify/countries)：按国家的渠道顺序和可用性
- [发送方与品牌](/docs/guides/verify/senders)：接收方在各渠道看到的内容
- [Verify events](/docs/guides/verify/events)：你的回调消费者迁移到的事件

## Related resources

- [Verify phone numbers at signup](/learn/series/verify-phone-numbers-at-signup) (video)
- [What does OTP mean? One-time passwords explained](/explained/verify/what-does-otp-mean) (answer)
- [Customer verification](/verify-api) (product)
- [Build your first integration](/learn/paths/integration) (course)

[Get an implementation brief](/learn/workspace?topic=verify)
