消息通知接口接入指南:签名规则、模板报备与发送频率控制
企业业务通知与动态口令接口的完整接入指南:签名算法(与核验类接口不同,不含 tm)、code 参数的真实含义、模板与签名报备、1005/1008/1009 错误码排查与发送频率控制的三层设计。
企业业务里的注册、登录、支付确认流程,离不开向用户下发动态口令与业务通知。本文面向企业自有业务的服务端开发场景,以诺正通(nztapi.com)消息通知接口为例,讲清接入全链路:签名算法(注意:和实名核验接口不是一套)、模板与签名报备、发送频率控制、错误码排查,以及几个新手必踩的参数坑。
合规前提:消息接口仅面向完成企业资质审核的商户开放,用于自有业务用户的服务通知,不支持个人申请,也不面向非自有用户发送。
前置:开通与调用位置
企业短信通道的开通方式和核验接口一致:官网提交申请 → 人工审核发放 mall_id / appkey,按量计费、发送失败不计费。
第一条铁律:消息接口只能在你的服务端调用。 appkey 参与签名计算,如果写在 App / 小程序 / 浏览器里,等于把计费钥匙发给所有逆向者。同时接口仅限发送给使用你服务的用户(如订单通知、登录口令),这是服务商协议和《通信短信息服务管理规定》的共同要求。标准链路是:
客户端(获取验证码按钮) → 你的后端 → 短信 API → 运营商 → 用户手机
一、签名算法:先记住"三个没有"
消息接口(短信通道)和实名核验类接口(身份证/手机号/银行卡)虽然都是 MD5 参数签名,但规则完全不同。最容易照抄出事的地方:
短信签名 = md5( mall_id + phone + code + appkey )
核验类签名 = md5( mall_id + realname + [业务字段] + tm + appkey )
短信签名的三个没有:
tm 时间戳——照抄核验类代码把 tm 拼进去,必返 1002 签名错误;realname;title 不参与签名——模板固定内容只是请求参数,不进签名串。Python 实现(零依赖版,requests 同理):
import hashlib, os, json
import urllib.parse, urllib.request
MALL_ID = os.environ["NZT_MALL_ID"] APP_KEY = os.environ["NZT_APP_KEY"] SMS_URL = "http://api.nztapi.com/sms.php"
def send_sms(phone: str, code: str, title: str = "验证码") -> dict: # 签名只含 mall_id + phone + code + appkey,无 tm sign = hashlib.md5((MALL_ID + phone + code + APP_KEY).encode("utf-8")).hexdigest() data = urllib.parse.urlencode({ "mall_id": MALL_ID, "phone": phone, "title": title, "code": code, "sign": sign, }).encode() req = urllib.request.Request(SMS_URL, data=data, headers={ "Content-Type": "application/x-www-form-urlencoded; charset=utf-8"}) with urllib.request.urlopen(req, timeout=10) as resp: return json.loads(resp.read().decode())
成功返回是扁平结构(没有核验类那种 data/status 包装):
{"code": 1000, "message": "发送成功"}
注意 code 这里是整数,核验类的 data.code 是字符串——判断时别写错类型。
二、参数坑:code 不是你以为的那个 code
这是新手阶段最集中的翻车点,单独讲:
参数 code 的含义是"短信内容",不是"验证码数字"。 接口字段命名有历史包袱,但行为就是这样:你传什么,用户手机收到的就是什么。所以:
# ✘ 错误:用户手机只会收到裸的 "1234"
send_sms("13800138000", "1234")
✔ 正确:完整文案,且必须自带【签名】
send_sms("13800138000", "【诺正通】您的验证码是 1234,5 分钟内有效。")
为什么必须带【签名】三个字?运营商对短信实行签名实名制——内容里没有 【xxx】 格式的签名,通道会直接拒发,返回码 1009 短信内容缺少签名信息。业务系统里"验证码"和"文案"本来就该分开设计:
import random
def make_code_text(code: str) -> str: return f"【你的签名】您的验证码是 {code},5 分钟内有效,请勿泄露。"
生成 6 位数字 → 自己存 Redis 设 5 分钟 TTL → 拼完整文案下发
验证逻辑在你自己服务端(比对 Redis 里的 code),短信通道只负责"把这段话送到手机"。
三、模板与签名报备:1005 敏感词的真实原因
验证码类内容基本自由(带签名即可),但两类内容要走提前报备,未报备的内容会被通道拦截,表现为 1005 短信内容存在敏感词 或发送失败:
【你的签名】 需要以企业资质向服务商报备(签名需与营业执照名称或品牌一致),一次报备全网通道生效;合规上多说一句:营销类短信除模板报备外,还受《通信短信息服务管理规定》(工信部令第 31 号)约束——未经用户同意不得发送、必须提供退订方式。用户回复退订后再发,号码会进通道黑名单,表现为 1007 号码在黑名单中。
四、频率控制:1008 不是故障是保护
同一号码短时间重复请求,通道返回 1008 发送频率过快。这不是坑,是运营商防止接口被滥用的保护机制。业务侧的标准配套:
获取验证码按钮 → 前端 60 秒倒计时禁用
→ 后端按手机号+IP 限频(如 1 次/60s,5 次/小时) → 敏感场景前面加图形验证码/滑块,先验再发短信
三层各挡一类攻击:倒计时挡误触、限频挡脚本、图形码挡批量。只做前端倒计时等于没做——接口是裸露给你的后端的。
五、错误码速查(消息接口独立码表)
tm;code 参与签名的原文与传输是否一致code 参数两个排查小技巧:
code 含中文参与签名时,签名用的原文和表单传输的值必须逐字节一致(都 UTF-8、不要一处 URL 编码一处没编码);mall_id 换成任意错误值若返回 1002,说明已进签名层;返回 1001 商户号不存在 则是账户层问题。六、生产落地清单
相关资源
docs/sms/ 目录合规提醒:短信通道仅限企业自有业务的验证码/通知场景使用,遵守《通信短信息服务管理规定》及运营商报备要求。