SMSPING · 使用指南
用 Node.js 调用 SMSPing API 接收短信验证码
这篇教程用大约 80 行、不依赖任何 npm 包的 Node.js 代码走完一个完整流程:查服务和国家、选一个有库存的报价、购买一次性号码、等短信、确认订单。下面所有接口和规则都来自接码 API 指南和开发者文档。上线前请再对照文档确认最新的字段和限制。
购买号码会扣除 SMSPing 账户里的真实余额。下面的脚本默认只做演练(dry run),加上
--buy才会真正购买。
准备
- Node.js 18 或更高版本。脚本只用 Node.js 内置的
fetch和randomUUID,不需要安装任何包。 - 一个有余额的 SMSPing 账户和 API 密钥:登录后在账户设置的 API 管理中生成密钥,完整密钥只显示一次。
- 密钥只放在服务端,比如环境变量
SMSPING_API_KEY。不要写进浏览器代码、URL 或公开仓库。
接口一览
所有接口都在 https://smsping.net/api/customer 下。
| 步骤 | 请求 | 是否需要密钥 |
|---|---|---|
| 列出服务和国家 | GET /catalog |
否 |
| 某个组合的价格和库存 | GET /catalog/offers?country=<id>&service=<id> |
否 |
| 购买号码 | POST /orders |
是 |
| 查询订单和短信 | GET /orders/:id |
是 |
| 收到短信后完成 | POST /orders/:id/finish |
是 |
| 等待中取消 | POST /orders/:id/cancel |
是 |
成功响应是 {"code": 0, "data": ...}。失败时返回非 2xx 状态码,响应体里有 message,通常还有 errorCode,比如 INSUFFICIENT_BALANCE(余额不足)或 PRICE_CHANGED(价格变了)。
第 1 步:查服务和国家的 ID
GET /catalog 是公开接口,返回 data.services 和 data.countries,每一项都有一个 id,后面的请求要用:
curl -s https://smsping.net/api/customer/catalog
比如 Telegram 是 telegram,美国是 usa。请以接口返回为准,不要自己猜。
第 2 步:选报价
GET /catalog/offers 必须同时带 country 和 service。每个报价有 operator(运营商)、price(币种见响应里的 currency,即 USD)和 stock(库存)。脚本只保留有库存的报价并选最便宜的,这是示例自己的选择,你也可以固定某个运营商。
第 3 步:带幂等键购买
POST /orders 需要 country、service、operator、expectedPrice(刚查到的价格)和 idempotencyKey(16 到 100 位,只能是字母、数字、_ 或 -)。
- 发请求之前先把幂等键存下来。如果请求超时,同一笔购买要用同一个键、同样的参数重试;换新键可能会再买一单。
- 遇到
PRICE_CHANGED,重新查报价,确认新价格后再买。 - HTTP 200 不代表已经分配到号码,要看
data.status。如果是review,保留订单号,查询该订单或联系客服,不要重复购买。
第 4 步:轮询验证码
调用 GET /orders/:id,两次查询至少间隔 5 秒。status 变成 received 时,验证码在 code 字段里(短信内容在 sms 里)。其他终态有 finished、canceled、timeout、failed 和 review。脚本里 10 分钟停止轮询是我们自己设的本地保护,不是 API 规定。
第 5 步:完成或取消
收到短信后调用 POST /orders/:id/finish。还在等待时可以调用 POST /orders/:id/cancel。只有确认取消或超时且没收到短信才会退款,所以请检查最终的 status 和 refunded 字段。
频率限制
目前每个账户每分钟 60 次请求。收到 HTTP 429 时,按 Retry-After 头里的秒数等待后,重发同一个请求。如果没有这个头,脚本默认等 5 秒,这是示例自己的选择。
完整脚本
// Receive an SMS verification code with the SMSPing API (Node.js 18+, built-in fetch, no dependencies).
// Set SMSPING_API_KEY in your environment. Nothing is bought unless you pass --buy.
import { randomUUID } from "node:crypto";
const API = "https://smsping.net/api/customer";
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
async function call(method, path, { params, body, auth = false } = {}) {
const url = new URL(API + path);
for (const [k, v] of Object.entries(params || {})) url.searchParams.set(k, v);
const headers = { Accept: "application/json" };
if (auth) headers.Authorization = "Bearer " + process.env.SMSPING_API_KEY;
if (body) headers["Content-Type"] = "application/json";
for (let i = 0; i < 5; i++) {
const res = await fetch(url, { method, headers, body: body ? JSON.stringify(body) : undefined });
if (res.status === 429) { // rate limited: wait Retry-After seconds, then resend the same request
await sleep(1000 * Number(res.headers.get("retry-after") || 5));
continue;
}
const payload = await res.json();
if (!res.ok || payload.code !== 0) {
throw new Error(`HTTP ${res.status} ${payload.errorCode}: ${payload.message}`);
}
return payload.data;
}
throw new Error("still rate limited after 5 tries");
}
async function cheapestOffer(country, service) {
const data = await call("GET", "/catalog/offers", { params: { country, service } });
const inStock = data.offers.filter((o) => o.stock > 0);
if (inStock.length === 0) throw new Error("No stock for this country/service right now.");
return inStock.reduce((a, b) => (b.price < a.price ? b : a));
}
async function waitForCode(orderId, maxWaitMs = 10 * 60 * 1000) {
const deadline = Date.now() + maxWaitMs;
while (Date.now() < deadline) {
const order = await call("GET", `/orders/${orderId}`, { auth: true });
if (order.status === "received") return order;
if (["canceled", "timeout", "failed", "review", "finished"].includes(order.status)) {
throw new Error(`Order ended with status=${order.status}, refunded=${order.refunded}`);
}
await sleep(5000); // the docs ask for at least 5 seconds between queries
}
throw new Error("No SMS yet; cancel the order or check it again later.");
}
async function main() {
const country = "usa", service = "telegram"; // IDs come from GET /catalog
const offer = await cheapestOffer(country, service);
const orderBody = {
country,
service,
operator: offer.operator,
expectedPrice: offer.price,
idempotencyKey: randomUUID().replaceAll("-", ""), // keep it: reuse it if you retry this same purchase
};
console.log("Cheapest offer:", offer);
if (!process.argv.includes("--buy")) {
console.log("Dry run. Would POST /orders with:", orderBody);
return;
}
let order = await call("POST", "/orders", { body: orderBody, auth: true }); // spends real balance
if (!["pending", "waiting"].includes(order.status)) {
throw new Error(`Not waiting for SMS: status=${order.status}`); // HTTP 200 alone is not success
}
console.log("Number:", order.phone);
order = await waitForCode(order.id);
console.log("Code:", order.code);
await call("POST", `/orders/${order.id}/finish`, { auth: true });
}
main().catch((err) => {
console.error(err.message);
process.exit(1);
});
运行:
把脚本保存为 receive_sms.mjs(用 .mjs 扩展名,Node 不需要 package.json 就能运行 import 语句)。
export SMSPING_API_KEY="你的密钥" # 账户设置 > API 管理
node receive_sms.mjs # 演练:只显示报价和将要提交的订单
node receive_sms.mjs --buy # 真正购买(扣真实余额)并等待验证码
注意事项
- 密钥只能访问所属账户的数据;重置、停用密钥,或修改密码、邮箱后,旧密钥会失效。
- 号码是一次性激活号码。购买前请确认目标服务接受你选择的号码类型。
- 想用 Python?请看本教程的 Python 版。
常见问题
查价格需要 API 密钥吗?
不需要。/catalog 和 /catalog/offers 是公开的;订单、短信和余额接口需要 Bearer 密钥。
购买请求超时了,要换新的幂等键重试吗?
不要。用同一个 idempotencyKey 和同样的参数重试,并检查已有订单。只有新的、独立的购买才用新键。
多久可以查一次短信?
至少间隔 5 秒,同时不超过账户每分钟 60 次的限制。
准备开始?
登录平台生成 API 密钥,并在开发者文档里核对当前接口、字段和限额。购买前,请确认所选号码类型被目标服务接受。
打开开发者文档