# 从其他服务商迁移 SMS

按照本指南将生产环境的 SMS 从其他服务商迁移到 Bird。在当前服务商中以不同方式处理的两件事决定了你能否在此发出第一条消息，因此它们排在代码之前：你发送到的国家，以及你使用的发送方。完成这两项之后，移植发送调用、迁移退订列表、将投递报告指向 webhook，并在切换真实流量之前对模拟目的地进行测试。

迁移清单：

1. [启用目的地国家](#1-启用目的地国家)
2. [设置发送方](#2-设置发送方)
3. [映射发送调用](#3-映射发送调用)到 `POST /v1/sms/messages`
4. [迁移退订列表](#4-迁移退订列表)
5. [将投递报告切换到 webhook](#5-将投递报告切换到-webhook)
6. 在正式切换前[对模拟目的地进行测试](#6-对模拟目的地进行测试)

第 3、4、5 步取决于你要离开的服务商。你的[服务商指南](#从特定服务商迁移)包含逐字段的载荷映射、状态与事件的转换，以及如何导出退订列表。

从第 1 步和第 2 步开始。发送方注册是 SMS 迁移中耗时最长的环节：运营商和注册机构的审核可能比代码变更还久。在确定切换日期之前先评估这两项的范围。

## 1. 启用目的地国家

你的工作区有一个默认拒绝的目的地白名单，初始状态下仅启用了你所在组织的本国。向其他国家发送消息会在 Bird 解析发送方之前返回 `422 SMSDestinationNotEnabled`，因此即使你完整移植了集成，第一条国际消息仍会失败，直到你开放该国家。

在 [**SMS** > **Destinations**](https://bird.com/dashboard/w/sms/destinations) 下启用你当前发送到的每个国家。从你当前服务商的消息日志中获取列表，而不是凭记忆：遗漏一个国家会在切换当天造成无声的缺口，启用一个从未使用的国家则是不必要的风险暴露。默认拒绝还能限制 SMS 欺诈造成的损失，即欺诈流量被发送到高价号段并向你计费。

## 2. 设置发送方

在自由文本发送中，`from` 是收件人看到的发送方，它有三种形态：字母数字发送方 ID、你的工作区拥有的 E.164 格式电话号码，或短代码。哪种形态可用取决于目的地国家，不适用于该国的发送方会被拒绝，并返回一个说明原因的 `422`。[发送 SMS](/docs/guides/sms/sending-sms#sender) 中有每种形态的规则。

如何获取每种发送方：

- **字母数字发送方 ID** 在 [**SMS** > **Senders**](https://bird.com/dashboard/w/sms/senders) 下自行创建。如果目的地国家要求注册发送方 ID，请在该处提交注册并等待批准后再路由流量。
- **通过本地长号码发送的美国商业流量**需要相应的 10DLC 品牌和活动，在 [**SMS** > **10DLC**](https://bird.com/dashboard/w/sms/10dlc) 下设置，而免费号码和专用短代码有各自的验证或申请流程。美国完全不接受字母数字发送方 ID，因此在其他地方通用的欧洲发送方 ID 在美国没有对应方案。
- **号码**通过 Numbers 工作流获取，可用性和托管配置取决于号码类型和目的地。查看 [SMS numbers](/products/sms/numbers) 了解正确的获取路径；添加字母数字发送方不会获取号码。
- **保留你当前的号码**不支持自助操作：Bird 没有你可以从控制台发起的号码转入流程。如果你的订阅者会回复你当前拥有的号码，请在安排切换日期之前联系支持团队发起号码转入，并将号码转入和代码变更作为两个独立事件来规划。

[系统模板发送](/docs/guides/sms/templates)使用不同的请求格式。它仍然需要相应的目的地和收件人权限。它提供消息正文、类别和发送方，因此不能同时传入 `from`，Bird 会选择一个对目的地有效的发送方。

## 3. 映射发送调用

单条发送的端点是 [`POST /v1/sms/messages`](/docs/api/reference/create-sms-message)。使用 `to`、`from`、`text` 和 `category` 构建 JSON 载荷，成功调用返回 `202 Accepted` 及一个以 `sms_` 为前缀的消息 ID。投递发生在响应之后，通过 webhook 事件和读取端点到达你。完整载荷请参阅[发送 SMS](/docs/guides/sms/sending-sms)；从你当前载荷的逐字段映射在你的[服务商指南](#从特定服务商迁移)中。

在移植代码之前，注意以下差异：

- **每个请求只有一个收件人。** Bird 没有收件人数组。如果你当前的服务商将一次调用扇出到多个号码，这里需要改为每个收件人一次调用，或在一个请求中使用一个[批量](/docs/guides/sms/sending-sms#batch-sending)发送独立消息。
- **自由文本中必须指定 `category`**，可选值为 `transactional`、`marketing`、`authentication` 或 `service`。大多数服务商从活动或发送方推断意图；这里你需要逐条消息声明。如果目的地国家要求注册发送方，该注册是针对特定类别批准的，超出该类别的发送会被 `422 SenderCategoryNotPermitted` 拒绝。注意，发送方的 `active` 状态无法提前告知这一点，因为它的报告不涉及任何类别；请改为查阅各国要求。在移植时就设置正确，而不是将所有内容默认为同一个值。
- **消息正文受分段上限限制，Bird 不会截断。** 过长的正文会被 `422` 拒绝。非 GSM-7 字符会使每个分段的容量减少一半以上，因此如果你当前的服务商静默地将弯引号和破折号转写为基本字符，请设置 [`options.smart_encoding`](/docs/guides/sms/sending-sms#segments-and-encoding) 以保持你习惯的分段计数。该选项默认关闭，因为它会修改你编写的正文。
- **使用 `tags` 作为筛选维度，使用 `metadata` 存放上下文信息。** 标签是 `{name, value}` 键值对，可用于筛选和分析切片；元数据是任意的 JSON，由 Bird 存储、在读取时返回，并在每个 webhook 事件中回传。旧服务商的单个客户端引用字段通常映射到 `metadata`。
- **单条发送的定时调度和出站 MMS 需要单独规划。** `scheduled_at`、`media_urls`、`validity_period` 和每收件人的 `personalization` 是[保留字段](/docs/guides/sms/sending-sms#reserved-fields)，会被 `422 SMSUnsupportedFeature` 拒绝。这部分集成不随其余内容一起迁移：在你自己的队列中保留定时发送，并在预定提交时间调用发送端点。对于受众活动，请单独评估 [Broadcasts](/products/sms/marketing/campaigns)；广播不是端点字段的简单重命名。
- **使用 `Idempotency-Key` 进行有限次重试。** 为每条逻辑消息发送唯一的密钥，并在三小时重放窗口内对相同请求的重试复用该密钥。重放可减少重复请求，但不是精确一次投递保证。参阅[幂等性](/docs/guides/idempotency)。

## 4. 迁移退订列表

在第一次生产发送**之前**导入你的退订记录。向已告知旧服务商停止接收消息的人发送消息是导致迁移失败的合规问题，运营商和监管机构都不会关心是哪家供应商丢失了记录。

Bird 抑制覆盖的是**发送方与订阅者的配对**，这可能比你旧服务商的服务、配置或账户级别的屏蔽范围更窄。确保在每个相关的发送方和项目中保留该用户的实际退订意愿。使用 [`POST /v1/sms/suppressions`](/docs/api/reference/create-sms-suppression) 逐对添加：

```bash
while IFS=, read -r destination originator; do
  curl -s -X POST https://us1.platform.bird.com/v1/sms/suppressions \
    -H "Authorization: Bearer $BIRD_API_KEY" \
    -H "Content-Type: application/json" \
    -d "{\"destination\": \"$destination\", \"originator\": \"$originator\"}"
done < opt-outs.csv
```

同样的导入可以通过 CLI 以 `bird sms suppressions add --destination +15550001234 --originator +15557654321` 方式执行。

关于导入需要了解两点：

- **发送方特定的抑制要求两端都填写。** 工作区范围的退订属于单独的[偏好所有者](/docs/guides/sms/opt-outs-and-keywords#opting-out-of-every-sender)。该调用是幂等的：`201` 记录一条新的抑制，`200` 返回已存在的手动抑制，因此重新运行未完成的导入是安全的。
- **导入的配对会获得 `reason: manual`，这会阻止所有类别包括事务性消息。** 这比 Bird 根据停止关键词自行记录的抑制更严格。如果订阅者仅退订了营销类消息，请慎重决定是否导入该配对。

在停用旧代码之前，先检查现有的关键词和偏好行为。Bird 会自动回复支持的关键词，并在其国家目录适用时记录抑制。对于不支持的请求、更广泛的偏好以及其他联系渠道，保留你自己的处理逻辑。自定义活动关键词和回复使用[关键词规则](https://bird.com/dashboard/w/sms/keyword-rules)。参阅[退订与关键词](/docs/guides/sms/opt-outs-and-keywords)了解覆盖范围和作用域。

## 5. 将投递报告切换到 webhook

使用 [`POST /v1/webhooks`](/docs/api/reference/create-webhook) 注册一个端点，并为其订阅明确的事件类型列表。这是大多数服务商迁移时需要的结构性变更：不再是每条消息或每个号码配置一个回调 URL，而是你的工作区拥有端点，每个端点订阅它需要的事件。

Bird 的事件名称遵循 `resource.action`。正常路径为 `sms.accepted`，然后 `sms.sent`，然后 `sms.delivered`，`sms.undelivered`、`sms.failed`、`sms.expired` 和 `sms.rejected` 覆盖其余情况，`sms.received` 承载对你号码的回复。从你当前服务商状态术语的转换在你的[服务商指南](#从特定服务商迁移)中，每个事件的载荷在 [SMS events](/docs/guides/sms/events) 中。

关联关系可以直接移植。每个事件携带 `sms_id`、`workspace_id`、`to` 和 `from`，并回传发送时的 `tags` 和 `metadata`，因此你的处理程序可以直接从事件中读取你自己的标识符，无需反查消息。

处理程序需要移植的两个机制：

- **投递使用 [Standard Webhooks](https://www.standardwebhooks.com) 签名**，通过 `webhook-id`、`webhook-timestamp` 和 `webhook-signature` 请求头，对 `{id}.{timestamp}.{raw body}` 进行 HMAC-SHA256 签名。使用自有签名方案的服务商需要替换验签逻辑；具体方法在 [Webhooks & events](/docs/guides/webhooks#verify-signatures) 中。
- **投递是至少一次且无序的。** 使用 `webhook-id` 去重，按载荷中的 `timestamp` 排序，切勿按到达顺序排序。

入站消息使用相同模型。在工作区级别订阅一次 `sms.received`，而不是为每个号码配置入站 URL，并注意 Bird 在记录抑制之后仍会为匹配停止关键词的回复触发 `sms.received`。

## 6. 对模拟目的地进行测试

Bird 为一组测试目的地合成投递结果，因此你可以用真实的 API 响应和真实的签名投递来验证你移植的发送路径和 webhook 处理程序，而无需真实手机。这些号码与多个服务商用于测试凭据的号码相同，发送到其中任何一个的消息不会到达运营商。

| 目的地         | 你的集成看到的结果                                        |
| -------------- | --------------------------------------------------------- |
| `+15005550001` | 提交时被 `invalid_destination` 拒绝                       |
| `+15005550002` | `sms.sent`，然后 `sms.undelivered` 附带 `unreachable`     |
| `+15005550003` | `sms.sent`，然后 `sms.failed` 附带 `provider_unavailable` |
| `+15005550004` | `sms.sent`，然后 `sms.failed` 附带 `blocked_by_carrier`   |
| `+15005550006` | `sms.sent`，然后 `sms.delivered`                          |
| `+15005550009` | `sms.sent`，然后 `sms.failed` 附带 `recipient_opted_out`  |

有三个条件需要注意，前两个常在新工作区上被忽视：

- 这些是美国号码，因此必须在 Destinations 下**启用美国**，并且 `from` 必须是对美国有效的发送方。字母数字发送方 ID 在美国会被拒绝。
- **模拟发送会按目的地的正常费率计费。** 虽然不会到达真实手机，但钱包扣费是真实的，因此请控制冒烟测试的规模。
- 结果完全由目的地决定。没有单独的测试凭据，也没有需要关闭的测试模式。

一个可行的冒烟测试包括：向 `+15005550006` 发送并断言你的处理程序经历 `sms.accepted` 到 `sms.sent` 到 `sms.delivered` 的流转；向 `+15005550002` 和 `+15005550009` 发送并断言你的失败处理和退订处理在正确的 `error` 代码上触发；再向你控制的真实手机发送一条消息，确认发送方和正文按预期显示。

然后按流量比例逐步切换，而不是一次性全部切换。将一小部分生产发送迁移到 Bird，在 [SMS 日志](/docs/guides/sms/sms-log)和[指标](/docs/guides/sms/tracking-and-metrics)中观察投递率和错误码，与旧服务商在相同路由上的表现对比，数据正常后逐步提高比例。保持旧集成可部署状态，直到第一个完整计费周期的数据正常。

## 从特定服务商迁移

- [Twilio](/docs/guides/sms/migrate/twilio)：表单编码的 `PascalCase` 迁移到 JSON，Messaging Services 迁移到发送方，`StatusCallback` 迁移到订阅式 webhook
- [Plivo](/docs/guides/sms/migrate/plivo)：`src` 和 `dst` 迁移到 `from` 和 `to`，Powerpacks 迁移到发送方，DND 配对迁移到抑制
- [Telnyx](/docs/guides/sms/migrate/telnyx)：与 Bird 最接近的发送方式，messaging profiles 拆分为发送方和订阅，profile 级别的退订迁移到配对
- [Bandwidth](/docs/guides/sms/migrate/bandwidth)：两个主机合并为一个，`applicationId` 回调迁移到工作区 webhook，退订列表由你自己的应用已持有
- [Sinch](/docs/guides/sms/migrate/sinch)：批量发送迁移到单条发送，`body` 迁移到 `text`，群组成员关系重建为抑制
- [Infobip](/docs/guides/sms/migrate/infobip)：三层载荷扁平化，每账户 base URL 迁移到区域主机，Blocklist 展开为配对
- [Bird Connectivity Platform](/docs/guides/sms/migrate/connectivity-platform)：`rest.messagebird.com` API、`originator` 和 `recipients` 迁移到 `from` 和 `to`，`reportUrl` GET 回调迁移到签名 webhook

## 后续步骤

- [比较 SMS 服务商](/products/sms/compare)：评估产品工作流和迁移注意事项

- [发送 SMS](/docs/guides/sms/sending-sms)：完整的发送载荷、发送方、分段以及异步 202 模型
- [退订与关键词](/docs/guides/sms/opt-outs-and-keywords)：Bird 为你自动回复的内容，以及如何管理抑制
- [SMS events](/docs/guides/sms/events)：事件词汇表和每个事件的载荷
- [Webhooks & events](/docs/guides/webhooks)：端点设置、签名验证、重试和重放

## Related resources

- [Choose a sender for your markets](/explained/sms/which-sms-sender-type-should-i-use) (answer)
- [Check your message segments](/tools/sms-segment-calculator) (tool)
