短信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 编码」范式:
将请求参数(除签名字段外)按 key 字典序升序 排列;
拼接为
key1=value1&key2=value2...;
前后拼接密钥(或使用 HMAC);
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,签名与发送使用同一份字节 |
时间戳单位错误 | 返回 | 先确认秒还是毫秒( |
参数值未做 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 code3.2 频控规则(通用基线)
维度 | 典型限制 | 备注 |
同号码 | 1 条 / 60 秒 | 验证码通道硬限制 |
同号码日限额 | 5~10 条 / 天 | 超限运营商可能直接拦截 |
通道 QPS | 验证码常见 200~2000 | 高峰前找服务商提升配额 |
营销时段 | 工作日 8:00–22:00 | 节假日及夜间禁发 |
手机号格式 | 11 位,1 开头 | 入库前用正则 |
避坑 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}), 2004.3 回执避坑清单
坑 | 现象 | 解决 |
回执接口超时 | 网关判定失败 → 疯狂重推 | 处理逻辑异步化(先入队,立刻返回 200) |
不做幂等 | 同一条回执处理多次,计数/通知重复 | 以 msgId 建唯一索引, |
只信「提交成功」 | 发送接口返回 OK ≠ 用户收到 | 业务成败以回执 |
计费对不上账 | 网关按 fee 条数计费,业务按发送条数算 | 对账以回执 fee 字段 + 网关日账单为准 |
回执地址配置错 | 永远收不到回执 | 上线前用「发送→收回执」全链路演练一遍 |
忽略重推机制 | 网关按退避策略重推多次 | 接口必须幂等,且保留 7 天以上回执日志 |
避坑 4:验证码业务的"状态回执补救"。 若回执显示
UNDELIV/EXPIRED,验证码已失效,应在业务层自动触发「语音验证码」或重发通道作为降级,而不是让用户干等。
五、上行短信(用户回复)
配置上行推送地址后,用户回复内容会推送到你的接口(mobile + content + destNumber);
典型用途:营销短信回复「TD」退订——必须实现退订黑名单,收到 TD 后写入 suppress list,否则属于违规,通道可能被关停;
上行内容含中文,注意编码与emoji过滤。
六、安全与合规红线
手机号等个人信息加密存储,日志脱敏(
138****5678);
营销短信必须带签名与退订方式("回复TD退订");
禁止发送内容:涉政涉恐、赌博、贷款导流(无资质)、刷单等——运营商标识 + 客诉会直接封通道;
验证码接口必须加图形/行为验证码防打码平台薅接口,否则一夜被盗刷几千条很常见;
密钥、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 文档为准。