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 密钥,并在开发者文档里核对当前接口、字段和限额。购买前,请确认所选号码类型被目标服务接受。

打开开发者文档