TBS社区

短信API对接全攻略:从签名计算到状态回执的实战避坑指南

铁布衫短信平台
铁布衫短信平台 楼主 发布于

短信API对接全攻略:从签名计算到状态回执的实战避坑指南

适用对象:准备接入 106 短信通道(验证码 / 通知 / 营销)的后端开发与运维人员。 本文以通用网关协议为蓝本,,细节参数以你所用服务商文档为准。


一、对接前必须搞清楚的 6 件事

#

事项

说明

踩坑概率

1

资质报备

企业营业执照、网站/APP 备案、签名归属证明(域名备案或商标证)

★★★★

2

签名(SignName)

短信开头【XX公司】,需与资质主体一致,个人主体不能用公司签名

★★★

3

模板报备

验证码/通知模板要带变量占位符(如 ${code}),营销模板审核最严

★★★★

4

通道类型

验证码/通知/营销是三条独立通道,速率、时段限制完全不同

★★★★

5

回执地址

状态回执(delivery report)推送地址必须提前在控制台或接口配置

★★

6

错误码文档

拿到完整错误码表(如运营商标识 SUBMIT_FAIL / DB:0103 等)

★★

避坑 1:签名与模板必须先报备后发送。 大量首日对接失败源于"发送接口返回 OK,但用户收不到短信"——实际是模板未审核通过,网关静默拦截。务必先在控制台确认模板状态为「已通过」。


二、签名(鉴权)计算:90% 的对接失败发生在这里

2.1 常见签名方案

主流平台普遍采用「参数排序 + 拼接 + 哈希 + URL 编码」范式:

  1. 将请求参数(除签名字段外)按 key 字典序升序 排列;

  1. 拼接为 key1=value1&key2=value2...;

  1. 前后拼接密钥(或使用 HMAC);

  1. MD5 / SHA-256 / HMAC-SHA256 计算后转 大写十六进制(注意大小写!是常见坑)。

2.2 Python 示例(通用网关)

import hashlib, time, urllib.parse, requests

API_URL   = "https://sms.example.com/v2/send"
APP_KEY   = "your_app_key"
APP_SECRET = "your_app_secret"

def send_sms(mobile: str, content: str) -> dict:
    params = {
        "appkey":   APP_KEY,
        "mobile":   mobile,          # 多号码逗号分隔,上限一般 1000
        "content":  content,          # 必须 UTF-8
        "timestamp": str(int(time.time())),   # 有效期通常 ±5 分钟
    }
    # 1. 按 key 升序排序
    sorted_params = sorted(params.items())
    # 2. 拼接 + 前后包裹密钥
    raw = APP_SECRET + "&".join(f"{k}={v}" for k, v in sorted_params) + APP_SECRET
    # 3. MD5 大写
    sign = hashlib.md5(raw.encode("utf-8")).hexdigest().upper()
    params["sign"] = sign
    resp = requests.post(API_URL, data=params, timeout=5)
    return resp.json()

2.4 签名避坑清单

坑

现象

解决

编码不一致

中文参数参与签名时用 GBK 编码但接口要求 UTF-8

全链路统一 UTF-8,签名与发送使用同一份字节

时间戳单位错误

返回 SIGNATURE_EXPIRED

先确认秒还是毫秒(time.time() 是秒,Java System.currentTimeMillis() 是毫秒)

参数值未做 URL 编码

含 &、=、中文的内容导致服务端解析错乱

发送前对 value 做标准 urlencode,签名以编码后或原始值视文档而定

排序规则记错

偶发签名失败,换个参数就挂

严格字典序,注意数字前空格与特殊字符

密钥泄露

余额被盗刷

Secret 只存服务端配置中心,绝不下发前端/客户端

重放攻击

同一请求被重复消费

timestamp + nonce,服务端做 5 分钟窗口与 nonce 去重


三、发送接口:频控、并发与幂等

3.1 关键参数与最佳实践

def send_code(mobile: str) -> str:
    # 1. 频控前置校验(业务层自己先拦,别全指望网关)
    assert not too_frequent(mobile), "同号码 60 秒内限发 1 条"
    code = f"{random.randint(0, 999999):06d}"
    result = send_sms(mobile, f"您的验证码是{code},5分钟内有效,请勿泄露。")
    if result.get("code") != 0:
        raise SendError(result.get("msg"))
    return code

3.2 频控规则(通用基线)

维度

典型限制

备注

同号码

1 条 / 60 秒

验证码通道硬限制

同号码日限额

5~10 条 / 天

超限运营商可能直接拦截

通道 QPS

验证码常见 200~2000

高峰前找服务商提升配额

营销时段

工作日 8:00–22:00

节假日及夜间禁发

手机号格式

11 位,1 开头

入库前用正则 ^1[3-9]\d{9}$ 校验

避坑 2:超长短信拆分计费。 67 字符(含签名【】)为 1 条计费单位,超出按 67 字符/条倍数计费。营销文案动辄 2~3 条费用,预算要按拆分条数算,别按"一条"算。

避坑 3:不要在业务高峰现场压测。 提前申请提高 QPS 配额,并用服务商提供的沙箱/测试签名验证,避免打挂生产通道影响真实用户。

3.3 幂等设计

  • 客户端传自定义 outId / bizId(如订单号),网关据此去重;

  • 发送前先落库 SENDING 状态,收到回执再更新为 DELIVERED / FAILED;

  • 网关超时(网络抖动)不要立刻重发——先查询接口确认 msgId 是否已受理,否则会产生重复扣费。


四、状态回执(Delivery Report):最容易漏掉的半场

4.1 回执生命周期

提交(Submit) → 网关受理(ACCEPT) → 运营商状态(Status)
   ├─ DELIVRD   发送成功,用户已收到
   ├─ EXPIRED   缓存超时(关机/信号差超过有效期)
   ├─ UNDELIV   不可达(空号/停机/黑名单)
   ├─ REJECTD   运营商拦截(内容敏感/签名不合规/投诉)
   └─ UNKNOWN   状态未知(需人工对账)

4.2 回执接收服务示例(Python Flask)

from flask import Flask, request, jsonify

app = Flask(__name__)

@app.route("/sms/report", methods=["POST"])
def report():
    # 网关通常推送 JSON 或 form,字段名以文档为准
    items = request.get_json(force=True).get("items", [])
    for it in items:
        msg_id   = it["msgId"]       # 发送接口返回的 ID
        status   = it["status"]      # DELIVRD / EXPIRED / UNDELIV ...
        err_code = it.get("errCode", "")
        fee      = it.get("fee", 1)  # 实际计费条数,注意按此对账
        # 幂等:按 msgId 唯一键更新,避免重复推送导致重复扣费/重复处理
        update_status_once(msg_id, status, err_code, fee)
    # 必须快速返回约定格式,否则网关会重推
    return jsonify({"code": 0}), 200

4.3 回执避坑清单

坑

现象

解决

回执接口超时

网关判定失败 → 疯狂重推

处理逻辑异步化(先入队,立刻返回 200)

不做幂等

同一条回执处理多次,计数/通知重复

以 msgId 建唯一索引,INSERT ... ON DUPLICATE KEY UPDATE

只信「提交成功」

发送接口返回 OK ≠ 用户收到

业务成败以回执 DELIVRD 为准

计费对不上账

网关按 fee 条数计费,业务按发送条数算

对账以回执 fee 字段 + 网关日账单为准

回执地址配置错

永远收不到回执

上线前用「发送→收回执」全链路演练一遍

忽略重推机制

网关按退避策略重推多次

接口必须幂等,且保留 7 天以上回执日志

避坑 4:验证码业务的"状态回执补救"。 若回执显示 UNDELIV/EXPIRED,验证码已失效,应在业务层自动触发「语音验证码」或重发通道作为降级,而不是让用户干等。


五、上行短信(用户回复)

  • 配置上行推送地址后,用户回复内容会推送到你的接口(mobile + content + destNumber);

  • 典型用途:营销短信回复「TD」退订——必须实现退订黑名单,收到 TD 后写入 suppress list,否则属于违规,通道可能被关停;

  • 上行内容含中文,注意编码与emoji过滤。


六、安全与合规红线

  1. 手机号等个人信息加密存储,日志脱敏(138****5678);

  1. 营销短信必须带签名与退订方式("回复TD退订");

  1. 禁止发送内容:涉政涉恐、赌博、贷款导流(无资质)、刷单等——运营商标识 + 客诉会直接封通道;

  1. 验证码接口必须加图形/行为验证码防打码平台薅接口,否则一夜被盗刷几千条很常见;

  1. 密钥、Secret 不进 Git,使用环境变量或密钥管理服务。


七、上线前 Checklist

  • 签名/模板已审核通过,控制台状态为「已通过」

  • 签名算法三种 case 验证通过:纯英文、含中文、含特殊字符

  • timestamp 单位(秒/毫秒)与服务商一致

  • 回执地址已配置,端到端收到 DELIVRD

  • 回执处理幂等(msgId 唯一索引)

  • 频控:同号码 60s/日限额 + 业务层图形验证码

  • 超长短信按 67 字符拆分测算成本

  • 失败降级:UNDELIV → 语音验证码 / 换通道重试

  • 日账单对账脚本上线(发送量 vs 计费条数 vs 回执成功量)

  • 错误码监控告警(如 SUBMIT_FAIL 比例 > 5% 触发告警)


八、常见错误码速查

错误码/标识

含义

处理

SIGNATURE_INVALID

签名计算错误

核对排序/编码/密钥/大小写

SIGNATURE_EXPIRED

时间戳过期

校准服务器时间,核对秒/毫秒

TEMPLATE_NOT_PASS

模板未审核通过

控制台确认模板状态

MOBILE_INVALID

号码格式错误

校验 11 位 1 开头

FREQUENCY_LIMIT

触发频控

业务层限流 + 提示稍后再试

SUBMIT_FAIL / DB:0103

运营商拒收

查运营商错误码表,多为内容/黑名单

BALANCE_NOT_ENOUGH

余额不足

接入余额查询接口 + 余额告警


本文为通用实战指南,具体字段名、签名细节以你所接入平台的官方 API 文档为准。

0 条评论
回复帖子