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/smssubreddit 和官方 Telegram 频道上的报告描述了价值 20 美元到数千美元不等的余额被冻结,且没有恢复途径。 - 正在运行的集成严重中断。任何轮询
handler_api.php的服务都开始收到 HTTP 连接错误,在大多数 bug 跟踪器中,这意味着“断路器已触发,请通知团队”。 - 迁移窗口在几天内关闭,而不是几周。72 小时内,所有剩余的提供商都出现了排队。5sim、SMSPVA 和 SMS-MAN 都承认了容量压力。VerifySMS 能够应对,因为我们是该群体中最小的,并且有额外的余量,但情况确实非常紧张。
后续影响仍在持续。截至 2026 年 4 月,俄罗斯已有积极的小额索赔案件,至少有两个协调的社区诉讼试图追回被冻结的余额。这些都无助于你的代码,这就是为什么我们将重点放在你可以实际修复的部分。
第一部分: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 | 轮询短信到达 | 工作正常。返回 STATUS_WAIT_CODE, STATUS_OK:CODE, STATUS_WAIT_RETRY | GET /v1/rentals/{id} |
getPrices | 获取价格表 | 工作正常。以旧版 JSON 格式返回 VerifySMS 定价 | 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,该端点已单独记录。
第二部分:代码迁移演练
以下代码片段是我在 1 月份针对我们自己的暂存环境测试过的确切格式。我故意保持它们简单,以便你可以在不切换上下文的情况下将它们与自己的代码进行比较。
Python (requests)
唯一必需的更改是基本 URL。如果你已经将 API 封装在一个小型客户端模块中,那么差异只有一行。
import os
import requests
# BEFORE
# BASE_URL = "https://sms-activate.org/stubs/handler_api.php"
# AFTER
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)
# Mark as unused so we get the refund
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";
// BEFORE
// const BASE_URL = "https://sms-activate.org/stubs/handler_api.php";
// AFTER
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
// BEFORE
// const BASE_URL = "https://sms-activate.org/stubs/handler_api.php";
// AFTER
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"
)
// BEFORE
// const baseURL = "https://sms-activate.org/stubs/handler_api.php"
// AFTER
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,保留其余部分,并让现有的错误处理继续。
第三部分:可能让你吃亏的陷阱
这些是兼容层忠实于 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 会确认验证成功。兼容层也以同样的方式运行。搜索你的代码中的 setStatus,并确保只有在确定验证成功后才运行接受代码 6 的分支。
超时和断路器
SMS-Activate 在负载下有时会返回一个 200 响应但正文为空,而不是 HTTP 错误。防御性客户端会将调用封装在超时中,并将空正文视为重试信号。VerifySMS 在兼容层上永远不会返回空正文。如果你的客户端仍然将空正文视为重试,它将因为重试命中不同的租赁 ID 而消耗预算。更安全的模式是检查已知的响应前缀(ACCESS_、STATUS_、BAD_),并将任何其他内容视为硬故障,而不是瞬态故障。
速率限制从每个密钥变为每个 IP
SMS-Activate 的速率限制是基于 API 令牌的。VerifySMS 的速率限制是基于 API 令牌和源 IP 地址的组合,因为我们看到大量来自抓取脚本的滥用,这些脚本跨越一个机器人网络共享一个密钥。对于来自单个服务器或负载均衡池的正常生产流量,这通常是不可见的。如果你运行共享一个暂存密钥的分布式 CI 作业,当整个集群同时启动时,你可能会第一次看到 429 错误。解决方法是让金丝雀在一个节点上运行一天,然后再进行扩展。
退款时机感觉是即时的,因为它实际上是即时的
这与其说是一个陷阱,不如说是一个惊喜。SMS-Activate 的退款需要几个小时才能显示在你的余额中,而 VerifySMS 的退款会在 60 秒内出现。如果你的成本跟踪器按计划读取余额,它会将退款注册为旧系统会错过的贷项。对账仪表板有时会在第一天将此标记为异常。
第四部分:价格比较现实情况
在关闭之前,SMS-Activate 是市场的最低价。俄罗斯号码每次验证的成本为 0.03 美元到 0.05 美元,大批量购买者甚至支付更少。那个底线已经消失了。以下是截至 2026 年 4 月,对于最常见的服务,剩余提供商的价格情况,摘自每个提供商在的公开定价页面:
| 服务 | 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 是美国的 premium tier,而 VerifySMS 则处于中间位置,大多数服务的基础价格为 0.10 美元,只有最昂贵的美国非 VoIP 号码除外。如果你的预算是按照 SMS-Activate 的最低价格设定的,那么无论你选择哪个替代方案,每次验证的费用预计将是原来的两到五倍。
关于此表的两点说明。首先,每个提供商(包括 VerifySMS)都会根据运营商成本调整单个国家/地区的定价,因此在确定预算之前,请在自己的仪表板中确认当前价格。其次,每次成功验证的*有效*价格取决于退款率。一个标价为 0.08 美元但成功率为 70% 的提供商,其每次成功的成本高于一个标价为 0.10 美元、自动退款且成功率为 90% 的提供商。
第五部分:10 步迁移清单
这是我们在 1 月份引导用户完成的实际流程。它假设有一个具有仓库访问权限的开发人员、一个生产服务和一个暂存环境。如果你运行多个服务或单体仓库,请相应地增加金丝雀的百分比。
- 盘点每个调用点。运行
git grep -n 'sms-activate\.org\|handler_api\.php\|getNumber\|setStatus'并列出所有命中旧 API 的文件。如果你发现超过十几个,请先选择一个包装器模块并集中调用,然后再进行迁移。 - 获取 VerifySMS API 密钥。注册,添加少量余额,并为暂存环境生成一个作用域密钥。将生产密钥保存在仓库之外。
- 更改基本 URL。将 SMS-Activate 主机替换为
api.verifysms.app/compat/handler_api.php。不要更改查询字符串。单独提交此更改,以便 diff 清晰。 - 运行现有测试。如果测试命中真实 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 美元到更严格平台上的美国非 VoIP 号码的 0.25 美元不等。VerifySMS 的基线价格为 0.10 美元,并在仪表板上发布了按国家/地区划分的定价。
我需要更改我的轮询逻辑吗?
不需要。getStatus 调用以 SMS-Activate 使用的相同格式返回 STATUS_WAIT_CODE 和 STATUS_OK。3 到 5 秒的轮询间隔仍然有效。唯一的新行为是 VerifySMS 还在仪表板中公开了一个 webhook URL,因此如果你更喜欢事件驱动的流程,可以完全停止轮询。
如果兼容层将来被弃用怎么办?
兼容层被视为永久性公共接口。如果我们将来更改其行为,我们将发布至少六个月的弃用窗口,并附带完整的迁移说明。原生 VerifySMS JSON API 也已记录在案,因此你可以在方便的时候自行迁移到原生 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 年短信验证状况 — 对 8 家服务进行全面的独立基准测试,包含定价、流量和退款政策数据。
- 2026年最佳SMS-Activate替代方案 — 完整指南 — 面向非开发者的替代方案指南。
- VerifySMS与SMS-Activate全面对比 — 供仍在评估的团队进行并排比较。
- SMS 验证 API 指南 2026:Twilio, Vonage, MessageBird, Plivo — 当你准备好脱离兼容层时,原生 VerifySMS API 的演练。
准备好将迁移工作缩短到一个晚上?
创建 VerifySMS API 密钥 →包含沙盒模式 · 自动退款保证 · 200+ 国家/地区 · SMS-Activate 兼容层位于 /compat/handler_api.php
更多中文文章
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 验证 API 指南 2026:Twilio, Vonage, MessageBird, Plivo — 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
