SMS-Activate 2026迁移中心:开发者检查清单、API映射和退款比较
If you are still carrying handler_api.php calls in your repo pointed at sms-activate.org, this is the hub you open at 9pm on a Friday and close with a working integration before midnight. Real endpoint mapping, real code diffs, refund policy comparison, and the gotchas that will bite you between the line you change and the alert that wakes you up on Monday morning.
为什么有这个攻略
大多数“SMS-Activate 死了,这里有一份替代品列表”的帖子都忽略了真正耗费开发者时间的部分:代码。注册一个新的服务提供商只需要五分钟时间。但是,重写一个已经在生产环境中运行了多年的集成,伴随着未测试的边界情况和附加的成本跟踪器,需要的时间比你想象的要长。
在我们于 2026 年 1 月发布兼容层后,我们开始从每个尝试使用它的团队那里收到同样的三个问题:
- 哪些端点可以无缝映射,哪些需要手动更改?
- 如何在不重写代码的情况下保持成本跟踪和退款流程正常运行?
- 什么会在静默中出现问题,并在一天后以账单惊喜的形式出现?
本攻略按顺序回答这三个问题,并为您提供可以直接运行前审计的代码。
48 小时回顾:到底发生了什么
SMS-Activate 于 2025 年 12 月 29 日 突然关闭。没有任何预定维护公告,没有迁移工具,也没有公开通知。用户尝试登录时,看到的是一页提示服务已永久关闭。API 在几小时内返回连接重置。
三件事情迅速发生:
- 仪表板余额无法访问。
r/sms子版块和官方 Telegram 频道上的报告描述了 20 美元到几千美元的余额被冻结,无法恢复。 - 正在运行的集成服务突然中断。 任何调用
handler_api.php的服务都开始接收 HTTP 连接错误,这在大多数错误跟踪器中意味着“断路器跳闸,提醒团队”。 - 迁移窗口期在几天内关闭,而不是几周。 72 小时内,所有剩余的服务提供商都有一个队列。5sim、SMSPVA 和 SMS-MAN 都承认了容量紧张。VerifySMS 坚持了下来,因为我们是该组中最小的,并且有备用余量,但它确实非常接近。
余波仍在持续。截至 2026 年 4 月,俄罗斯和至少两起协调的社区诉讼正在尝试追回冻结的余额。这些都与您的代码无关,因此我们将重点关注您可以实际解决的部分。
第 1 部分:API 废弃映射
SMS-Activate 提供了位于 https://sms-activate.org/stubs/handler_api.php 的单个公共端点。每个操作都是该 URL 上的查询字符串参数。下表将每个主要操作映射到其 VerifySMS 等价操作。位于 https://api.verifysms.app/compat/handler_api.php 的兼容层接受完全相同的查询字符串形状。
| SMS-Activate 操作 | 用途 | VerifySMS 兼容层 | 原生 VerifySMS API |
|---|---|---|---|
getBalance | 返回美元余额作为文本 | 正常工作。返回 ACCESS_BALANCE:X.YY | GET /v1/balance 返回 JSON |
getNumbersStatus | 按国家划分的可用性 | 正常工作。返回旧的映射格式 | GET /v1/countries/availability |
getNumber | 租用一个号码用于服务 | 正常工作。返回 ACCESS_NUMBER:id:+phone | POST /v1/rentals |
setStatus | 确认或取消租用 | 正常工作。状态代码 1/3/6/8 行为相同 | POST /v1/rentals/{id}/status |
getStatus | 轮询 SMS 到达 | 正常工作。返回 STATUS_WAIT_CODE, STATUS_OK:CODE, STATUS_WAIT_RETRY | GET /v1/rentals/{id} |
getPrices | 获取价格表 | 正常工作。返回 VerifySMS 定价,格式为旧 JSON | GET /v1/prices |
getCountries | 国家代码映射 | 正常工作。返回旧的数字 ID 和 ISO-3166 代码 | GET /v1/countries |
getTopCountriesByService | 每个服务的热门国家 | 返回实时的 VerifySMS 数据,而不是缓存的 SMS-Activate 排名 | GET /v1/services/{id}/top-countries |
少数未被充分使用的 SMS-Activate 操作不能一对一映射。getRentServicesAndCountries 和长期租赁 API 是 SMS-Activate 特有的,没有兼容层。如果您的集成使用了这些,您应该迁移到原生的 VerifySMS 长期租赁端点 POST /v1/rentals/long,该端点单独记录。
第 2 部分:代码迁移演练
以下代码片段是我在 1 月份针对我们自己的预发布环境测试的确切形状。我故意保持它们枯燥,以便您可以阅读它们而不必切换到自己的代码。
Python(requests)
唯一需要的更改是基本 URL。如果您已经将 API 封装在一个小型客户端模块中,则差异只有一行。
import os
import requests
# 之前
# BASE_URL = "https://sms-activate.org/stubs/handler_api.php"
# 之后
BASE_URL = "https://api.verifysms.app/compat/handler_api.php"
API_KEY = os.environ["SMS_API_KEY"]
def get_number(service: str, country: int) -> tuple[str, str]:
resp = requests.get(BASE_URL, params={
"api_key": API_KEY,
"action": "getNumber",
"service": service,
"country": country,
}, timeout=30)
resp.raise_for_status()
# ACCESS_NUMBER:12345:+441234567890
status, rental_id, phone = resp.text.split(":", 2)
if status != "ACCESS_NUMBER":
raise RuntimeError(f"unexpected response: {resp.text}")
return rental_id, phone
def wait_for_code(rental_id: str, deadline_seconds: int = 180) -> str:
import time
start = time.monotonic()
while time.monotonic() - start < deadline_seconds:
resp = requests.get(BASE_URL, params={
"api_key": API_KEY,
"action": "getStatus",
"id": rental_id,
}, timeout=15).text
if resp.startswith("STATUS_OK:"):
return resp.split(":", 1)[1]
time.sleep(4)
# 标记为未使用,以便我们获得退款
requests.get(BASE_URL, params={
"api_key": API_KEY,
"action": "setStatus",
"status": 8,
"id": rental_id,
}, timeout=15)
raise TimeoutError(f"no code after {deadline_seconds}s")
Node.js(axios)
import axios from "axios";
// 之前
// const BASE_URL = "https://sms-activate.org/stubs/handler_api.php";
// 之后
const BASE_URL = "https://api.verifysms.app/compat/handler_api.php";
const API_KEY = process.env.SMS_API_KEY;
export async function getNumber(service, country) {
const { data } = await axios.get(BASE_URL, {
params: { api_key: API_KEY, action: "getNumber", service, country },
timeout: 30_000,
});
const [status, rentalId, phone] = data.split(":");
if (status !== "ACCESS_NUMBER") {
throw new Error(`unexpected response: ${data}`);
}
return { rentalId, phone };
}
export async function waitForCode(rentalId, deadlineMs = 180_000) {
const start = Date.now();
while (Date.now() - start < deadlineMs) {
const { data } = await axios.get(BASE_URL, {
params: { api_key: API_KEY, action: "getStatus", id: rentalId },
timeout: 15_000,
});
if (data.startsWith("STATUS_OK:")) return data.split(":")[1];
await new Promise((r) => setTimeout(r, 4000));
}
await axios.get(BASE_URL, {
params: { api_key: API_KEY, action: "setStatus", status: 8, id: rentalId },
timeout: 15_000,
});
throw new Error(`no code after ${deadlineMs}ms`);
}
PHP(curl)
<?php
// 之前
// const BASE_URL = "https://sms-activate.org/stubs/handler_api.php";
// 之后
const BASE_URL = "https://api.verifysms.app/compat/handler_api.php";
function sms_call(array $params): string {
$params["api_key"] = getenv("SMS_API_KEY");
$url = BASE_URL . "?" . http_build_query($params);
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 30);
$body = curl_exec($ch);
curl_close($ch);
return $body;
}
function get_number(string $service, int $country): array {
$resp = sms_call(["action" => "getNumber", "service" => $service, "country" => $country]);
[$status, $id, $phone] = explode(":", $resp, 3);
if ($status !== "ACCESS_NUMBER") {
throw new RuntimeException("unexpected: $resp");
}
return ["id" => $id, "phone" => $phone];
}
Go(net/http)
package sms
import (
"errors"
"fmt"
"io"
"net/http"
"net/url"
"os"
"strings"
"time"
)
// 之前
// const baseURL = "https://sms-activate.org/stubs/handler_api.php"
// 之后
const baseURL = "https://api.verifysms.app/compat/handler_api.php"
func call(params url.Values) (string, error) {
params.Set("api_key", os.Getenv("SMS_API_KEY"))
req, _ := http.NewRequest("GET", baseURL+"?"+params.Encode(), nil)
client := &http.Client{Timeout: 30 * time.Second}
resp, err := client.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
return string(body), nil
}
func GetNumber(service string, country int) (id, phone string, err error) {
body, err := call(url.Values{
"action": {"getNumber"},
"service": {service},
"country": {fmt.Sprint(country)},
})
if err != nil {
return "", "", err
}
parts := strings.SplitN(body, ":", 3)
if len(parts) != 3 || parts[0] != "ACCESS_NUMBER" {
return "", "", errors.New("unexpected: " + body)
}
return parts[1], parts[2], nil
}
Go 示例故意编写时没有使用任何第三方客户端,这样您可以将其放入最小的服务中而无需添加依赖项。在每种语言中,模式都是相同的:交换 URL,保持其他部分不变,并让现有的错误处理继续进行。
第 3 部分:会烧毁你的陷阱
这些是兼容层忠实于 SMS-Activate 的怪癖的地方,但这些怪癖本身会在您不注意时让您惊讶。
国家 ID 不是 ISO 代码
SMS-Activate 按自己的顺序编号国家:俄罗斯为 0,美国为 187,印度尼西亚为 6,等等。如果您的集成服务中硬编码了这些神奇数字,它们仍在兼容层上工作。如果您正在编写新代码,请优先使用 ISO-3166 alpha-2 形式(RU、US、ID),兼容层也接受该形式。不要在同一个调用站点中混合使用这两种样式,因为未来调试将非常痛苦。
状态代码 3 与 6
在 SMS-Activate 世界中,setStatus 操作代码 3 表示“请求另一个 SMS”,而代码 6 表示“确认代码有效”。这两个代码很容易在匆忙中交换,它们具有相反的计费结果:3 使您继续计费,6 确认成功验证。兼容层表现相同。请 grep 您的代码以查找 setStatus,并确保在确认验证成功后仅运行代码 6 的分支。
超时和断路器
SMS-Activate 在负载下有时会返回带有空主体的 200 状态码,而不是 HTTP 错误。防御性客户端在超时内封装调用,并将空主体视为重试信号。VerifySMS 兼容层永远不会返回空主体。如果您的客户端仍然将空主体视为重试,它将在网络不稳定时烧掉预算,因为重试将击中不同的租赁 ID。更安全的模式是检查已知响应前缀(ACCESS_、STATUS_、BAD_),并将其他所有情况视为硬故障,而不是瞬时故障。
速率限制从按密钥改为按 IP 地址
SMS-Activate 的速率限制是按 API 令牌进行的。VerifySMS 的速率限制是按 API 令牌和源 IP 地址组合进行的,因为我们看到大量滥用脚本共享一个密钥跨越一个僵尸网络。对于来自单个服务器或负载均衡池的正常生产流量,这不可见。如果您运行分布式 CI 作业,都共享一个临时密钥,您可能会在整个舰队首次启动时看到 429 状态码。解决方法是从一个节点运行金丝雀一天,然后再扩展。
退款时间感觉即时,因为它确实是即时的
这不是一个陷阱,而是一个令人愉快的惊喜。SMS-Activate 的退款需要几个小时才能出现在您的余额中,而 VerifySMS 的退款在 60 秒内出现。如果您的成本跟踪器按计划读取余额,它将在旧系统无法察觉的信用额度中注册为贷方。协调仪表板有时会在第一天将此标记为异常。
第 4 部分:价格比较现实检查
在关闭之前,SMS-Activate 是市场的基准。俄罗斯号码的验证费用为 $0.03 至 $0.05,大批量购买者支付的费用更低。这一基准已经消失。以下是 2026 年 4 月 7 日各服务提供商最常见服务的公开定价页面价格:
| 服务 | 5sim | TextVerified | SMSPVA | SMS-MAN | VerifySMS |
|---|---|---|---|---|---|
| WhatsApp / 俄罗斯 | $0.014 | — | $0.05 | $0.035 | $0.10 |
| WhatsApp / 美国 | $0.27 | $0.25 | $0.28 | $0.22 | $0.18 |
| Telegram / 俄罗斯 | $0.016 | — | $0.05 | $0.04 | $0.10 |
| Telegram / 美国 | $0.35 | $0.40 | $0.38 | $0.30 | $0.20 |
| Google / 印度尼西亚 | $0.07 | — | $0.08 | $0.06 | $0.10 |
模式很简单:5sim 和 SMS-MAN 在俄罗斯低价中占据优势,TextVerified 是美国高端市场,而 VerifySMS 处于中间位置,所有服务的基准价格为 $0.10。如果您的预算是根据 SMS-Activate 的基准价格调整的,那么无论您选择哪个替代服务,预计每笔验证费用都会增加两到五倍。
关于此表格的两个说明。首先,每个服务提供商(包括 VerifySMS)都会根据运营商成本提高或降低个别国家的价格,因此在承诺预算之前,请在自己的仪表盘上确认当前价格。其次,每笔成功验证的有效价格取决于退款比率。如果一个服务提供商的标价为 $0.08,成功率为 70%,而另一个服务提供商的标价为 $0.10,自动退款,成功率为 90%,那么后者每笔成功的实际成本更低。
第 5 部分:10 步迁移检查清单
这是我们在 1 月份为用户提供的实际操作步骤。假设有一个开发人员拥有仓库访问权限,一个生产服务和一个预发布环境。如果您运行多个服务或一个 monorepo,请相应地扩大金丝雀的百分比。
- 清点所有调用站点。 运行
git grep -n 'sms-activate\.org\|handler_api\.php\|getNumber\|setStatus'并列出所有调用旧 API 的文件。如果您发现的文件超过十二个,请先选择一个包装模块并集中调用。 - 获取 VerifySMS API 密钥。 注册,添加少量余额,并为预发布环境生成一个限定范围的密钥。不要将生产密钥放入仓库。
- 更改基准 URL。 将 SMS-Activate 主机替换为
api.verifysms.app/compat/handler_api.php。不要更改查询字符串。单独提交此更改,以便于清理。 - 运行现有的测试。 如果测试调用真实 API,请将它们指向预发布环境并监视形状不匹配。如果测试模拟 API,请在实时预发布端点上运行它们,以捕获合同漂移。
- 重新确认国家/地区 ID。 检查代码中的国家/地区常量。如果使用旧的数字 ID,它们仍然有效。如果有机会,请用 ISO-3166 代码替换它们,因为下一个开发人员会感谢您。
- 连接退款声明。 确认您的超时路径调用
setStatus且status=8。如果不这样做,您仍然会收到退款(我们会自动退款已过期的租约),但您的成本跟踪器将落后于实际情况。 - 更新成本跟踪器。 从
X-VerifySMS-Cost响应头中读取成本,而不是从定价表中解析。单个此更改使您的财务仪表盘准确到分。 - 监控。 为您的现有基准添加成功率、p95 延迟和退款比率警报。选择您可以捍卫的阈值,而不是您认为“没问题”的阈值。
- 金丝雀 5% 流量,持续 24 小时。 将一小部分生产流量路由到新端点。监视仪表盘,而不仅仅是警报。
- 切换其余流量。 一旦金丝雀窗口清理干净,移动剩余的 95% 流量,并将旧客户端代码注释掉(不要删除),以便在一个发布周期内进行快速回滚。
在下一个版本之后删除旧代码。不要将无效的调用站点保留超过一周,因为下一个接触该模块的人员会意外地将它们粘贴回新的集成中。
常见问题
SMS-Activate 兼容层是真实的 API 还是仅仅是一个存根?
它是位于 api.verifysms.app/compat/handler_api.php 的真实端点,接受 SMS-Activate 公共文档中的每个主要操作:getBalance、getNumber、getStatus、setStatus、getPrices 和 getCountries。在后台,请求被代理到我们的原生 API,因此您无需更改代码即可获得 VerifySMS 的定价、覆盖范围和退款行为。
我的旧 API 密钥是否仍然有效?
不。SMS-Activate API 密钥在服务关闭的那一天就停止验证了。您需要从 VerifySMS 获取新的密钥。注册,添加少量余额,并从仪表盘生成密钥。密钥长度相同,因此您可以将其粘贴到相同的环境变量中。
与 SMS-Activate 相比,退款如何工作?
SMS-Activate 要求您在 20 分钟内调用 setStatus 且状态代码为 8,以将号码标记为未使用,退款会在几个小时内手动处理。VerifySMS 接受相同的 setStatus 调用,并在 60 秒内将全额退款到您的余额中。如果您完全忘记调用 setStatus,我们的系统仍会在租约窗口过期后自动退款任何从未收到短信的号码。
支持哪些国家/地区?
VerifySMS 涵盖 200 多个国家/地区。SMS-Activate 提供的每个国家/地区都可在 VerifySMS 上使用,包括俄罗斯、印度尼西亚、越南、尼日利亚和美国。您可以保留现有的国家/地区 ID 映射或在任何时候迁移到 ISO-3166 alpha-2 代码。
定价是否相同?
不。SMS-Activate 每笔验证的 $0.03 至 $0.05 的最低价格已从开放市场消失。当前市场定价范围从 $0.10 到 $0.25 不等,具体取决于服务和平台。VerifySMS 以 $0.10 为基准,并公布仪表盘上的每个国家/地区的定价。
我需要更改我的轮询逻辑吗?
不。getStatus 调用以与 SMS-Activate 相同的格式返回 STATUS_WAIT_CODE 和 STATUS_OK。轮询间隔为 3 到 5 秒仍然有效。唯一的新行为是 VerifySMS 还在仪表盘上公开了一个 webhook URL,因此,如果您更喜欢事件驱动的流程,可以完全停止轮询。
如果兼容层被弃用,会发生什么?
兼容层被视为永久公共接口。如果我们更改其行为,我们将发布至少六个月的弃用窗口和完整的迁移说明。原生 VerifySMS JSON API 也已记录,因此您可以根据需要随时迁移到兼容层之外。
如何在不花钱的情况下测试?
VerifySMS 仪表盘提供了一个沙盒模式,无需扣除您的余额即可返回模拟的电话号码和预设的短信代码。在仪表盘中打开沙盒标志,或在任何请求中发送 X-Sandbox-Mode 头,以在上线前测试您的代码路径。
我可以从其他服务迁移吗?
是的。本指南是围绕 SMS-Activate API 编写的,因为大多数搁浅的代码都在那里,但相同的检查清单适用于从 5sim、SMS-MAN 或任何其他 handler_api 兼容服务的迁移。兼容层识别 handler_api.php 参数,而不管您之前调用的是哪个服务。
实际迁移需要多长时间?
对于具有几十个调用站点的单个服务集成,计划 2 到 4 小时的专注工作,加上 24 小时的金丝雀窗口,然后再切换全部流量。较大规模的多服务迁移,带有自定义错误处理、分析和重试,可以运行更长时间,但通常仍在一个工作日内完成。
我会丢失历史数据吗?
SMS-Activate 验证历史记录在服务关闭时下线,无法恢复。VerifySMS 维护您的帐户上的每个验证尝试的完整审计日志,期限为 12 个月,可从仪表盘和 /compat/handler_api.php?action=getHistory 扩展中访问。
这会影响我的 GDPR 或合规性吗?
VerifySMS 在英国注册,并遵循英国 GDPR。我们在隐私页面上公布了我们的 数据保留政策、子处理者和 DPA。如果您的先前设置需要与 SMS-Activate 的 DPA,请联系我们,我们将在一个工作日内签署相同的协议。
下一步
如果您已经读到这里,您已经拥有了所需的全部信息。从清点步骤开始,在将基本 URL 交换给队友审查之前,并在夜间运行金丝雀。游戏指南很小,但难点在于金丝雀窗口之后保持纪律,而不是在一次提交中切换所有内容。
站点相关阅读:
- 2026 年 SMS 验证状态 — 8 项服务的完整独立基准测试,包含定价、流量和退款政策数据。
- 2026年最佳SMS-Activate替代方案 — 完整指南 — 非开发人员指南,了解替代选项。
- VerifySMS与SMS-Activate全面对比 — 仍在评估的团队的并排比较。
- SMS 验证 API 集成指南 — 原生 VerifySMS API 演练,用于在兼容层上迁移。
准备好将迁移限制在单个晚上?
创建 VerifySMS API 密钥 →包含沙盒模式 · 自动退款保证 · 200+ 个国家/地区 · /compat/handler_api.php 上的 SMS-Activate 兼容层
Next steps
If you have read this far, you already have the pieces you need. Start with the inventory step, get the base URL swap in front of a teammate for review, and run the canary overnight. The playbook is small on purpose; the hard part is the discipline to stop after the canary window instead of cutting everything over in one commit.
Related reading on the rest of the site:
- State of SMS Verification 2026 — full independent benchmark of 8 services with pricing, traffic, and refund policy data.
- 2026年最佳SMS-Activate替代方案 — 完整指南 — non-developer guide to the replacement options.
- VerifySMS与SMS-Activate全面对比 — side-by-side comparison for teams still evaluating.
- SMS Verification API Integration Guide — native VerifySMS API walkthrough for when you are ready to move off the compat layer.
Ready to cut the migration to a single evening?
Create a VerifySMS API key →Sandbox mode included · Auto-refund guarantee · 200+ countries · SMS-Activate compat layer on /compat/handler_api.php