50 KiB
客户 HTTP 短信接口接入手册
接口版本:v1 · 手册修订:2026-09-14
适用对象:需要从自己的业务系统发送短信、查询短信结果或接收用户回复的开发人员。
本文说明 HTTP 接口的接入步骤、鉴权、请求与响应以及回调处理。所有号码、消息编号和响应示例均为说明用途,不是真实客户数据。GET 空请求体使用第 3 节规定的兼容摘要;每次请求都须重新生成请求唯一标识和签名。
建议先完成上行查询与签名校对,再接入发送和回调。完整命令与报文示例见第 11 节;复制示例不会执行请求。
1. 开始接入前
1.1 这套接口能做什么
| 你的业务需求 | 使用的能力 |
|---|---|
| 向一个手机号发送一条短信 | 单条发送接口 |
| 知道短信是否提交、送达或失败 | 短信状态查询接口 |
| 短信有回执时,由平台主动通知你的系统 | 回执回调(Webhook) |
| 获取手机用户回复的短信 | 上行列表、上行详情,或上行回调 |
“上行短信”就是手机用户回复到短信接入号的内容。“回执”是短信提交后的送达或失败结果。两者不是同一种通知。
本版只提供四个公开业务接口,不提供公开的批量发送、余额查询、签名管理或模板管理接口。签名、模板和应用配置通过平台页面办理。
1.2 开户、开通和凭据
依次完成以下准备:
- 由平台创建企业账号和企业应用,并开通该应用的 HTTP 接口及需要的发送、查询、回调能力。
- 登录客户端,进入 接口对接,选择正确的企业应用。在“接口概览”确认接口地址、能力、QPS、时间容差和 HTTP IP 白名单。
- 在“访问凭据”创建凭据,保存 Access Key 和 Secret。自助创建未开放时联系平台人员。
- 如果需要发送,先完成签名和模板审核、必要报备以及余额和可用路由准备。只开通 HTTP 不代表任意正文都可以发送。
- 如果需要回调,在“回调配置”分别保存回执、上行 HTTPS 地址,并保存各自的回调签名密钥。
Secret 只在创建或轮换时展示一次,后续末四位不能用于签名。凭据属于特定企业应用,不要将应用 A 的凭据用于查询应用 B 的短信。
| 名称 | 用途 | 是否放进请求头 |
|---|---|---|
| Access Key | 标识调用哪个企业应用的接口 | 放入 X-App-Key |
| Secret | 在你的服务端计算请求签名 | 不直接发送 |
| 回调签名密钥 | 验证平台推送到你系统的通知 | 不直接发送;与请求 Secret 分开保存 |
| 平台登录密码 | 登录客户端网页 | 不用于 HTTP 接口签名 |
凭据应由你的后端服务保管,不放在网页前端代码、公开仓库或日志中。轮换请求凭据时,先建立新凭据、切换调用,再吊销旧凭据;可同时有效的数量以应用配置为准。回调密钥轮换需协调接收端,当前不要假设平台同时用新旧密钥签名。
1.3 地址与文档入口
| 用途 | 地址 |
|---|---|
| 客户接口基础地址 | https://api.lisglo.com/api/openapi/v1 |
| 在线 Swagger | https://api.lisglo.com/api/client-docs |
| OpenAPI JSON | https://api.lisglo.com/api/client-docs-json |
| 客户端文档入口 | 接口对接 → 选择企业应用 → 接口文档 |
上述域名当前连接预生产真实业务系统。未开户也能查看文档,但业务调用需要有效凭据;Swagger 的调试按钮会发真实请求,不是匿名试用或模拟发送。测试环境使用平台另行提供的地址和凭据,不能混用。
建议第一次先查询上行列表。 即使没有上行数据,也可以用空列表确认接入流程;不要用发送短信来测试网络是否通畅。
2. 四个接口一览
以下路径均在域名 https://api.lisglo.com 后拼接,已包含 /api,不要重复添加。
| 方法 | 完整路径 | 成功状态 |
|---|---|---|
| POST | /api/openapi/v1/sms/messages |
202:请求受理 |
| GET | /api/openapi/v1/sms/messages/{messageId} |
200:返回短信状态 |
| GET | /api/openapi/v1/sms/uplinks |
200:返回上行列表 |
| GET | /api/openapi/v1/sms/uplinks/{uplinkId} |
200:返回上行详情 |
所有接口都需要下面的鉴权头。请求和响应使用 UTF-8;POST 请求体使用 JSON。成功响应直接返回对象,没有额外的 data 包装层;回调通知则有自己的 data 字段。
3. 每次请求怎样签名
3.1 请求头
| 请求头 | 必填 | 填写方式 |
|---|---|---|
X-App-Key |
是 | 该应用的 Access Key |
X-Timestamp |
是 | 当前 Unix 时间戳,单位为秒,例如 1789344000;不要传毫秒 |
X-Nonce |
是 | 本次 HTTP 请求的唯一标识,推荐 UUID v4,例如 550e8400-e29b-41d4-a716-446655440000;兼容原 8~128 字符规则 |
X-Signature |
是 | 按下文计算的 HMAC-SHA256,小写十六进制字符串 |
Idempotency-Key |
发送时必填 | 一次业务发送的稳定编号,8~128 字符,允许字母、数字、.、_、:、- |
Content-Type |
POST 使用 | application/json |
User-Agent |
否 | 可填写客户端名称和版本,便于排错 |
默认时间容差为正负 300 秒,具体以应用配置为准。服务器时钟应同步。每次请求包括网络重试都要生成新的 nonce;即使业务请求被限流,旧 nonce 也不能继续使用。
推荐用标准库生成 UUID v4,例如 Python str(uuid.uuid4())。当前服务端仍兼容字母、数字、下划线和短横线组成的 8~128 字符,不要求旧客户全部改成 UUID。不要使用订单号或仅有时间戳的值代替每次请求的新标识。
| 标识 | 代表的对象 | 同一次发送的网络重试 |
|---|---|---|
X-Nonce(请求唯一标识) |
一次 HTTP 调用,防止请求重放 | 重新生成 UUID v4 |
Idempotency-Key(业务幂等键) |
一次业务发送,防止重复创建短信 | 保持不变,并保持 body 原始字节相同 |
clientMessageId(客户短信编号) |
客户系统中的短信业务记录 | 保持不变 |
3.2 签名原文
按下面的固定顺序拼接字符串;BODY_HASH 是第 3.3 节或 POST 原始字节得到的摘要:
SIGNING_STRING = METHOD + "\n" + PATH + "\n" + TIMESTAMP + "\n" + NONCE + "\n" + BODY_HASH
SIGNATURE = HEX(HMAC_SHA256(UTF8(Secret), UTF8(SIGNING_STRING)))
公式中的 "\n" 是程序语言表示法,指一个 LF 字节 0x0A,不是反斜杠和字母 n 两个字符,也不是 Windows 的 CRLF(0x0D 0x0A)。这是签名字符串内部的规则,与 HTTP 报文的行结束符不同。最后不加换行、不加空格、不加引号。
& 仍用于实际 URL 的 query 参数连接,例如 limit=25&cursor=...,不用于替换本接口签名原文的分隔符。当前 query 不参与签名;保持此规则,不在文档修订中暗中改变协议。
以下是同一个签名原文的分行展示,便于逐项核对:
大写 HTTP 方法
请求路径
X-Timestamp 的原始字符串
X-Nonce 的原始字符串
请求体的 SHA256 十六进制摘要
然后用 Secret 的 UTF-8 字节作为 HMAC 密钥,对这五行的 UTF-8 字节计算 HMAC-SHA256,输出 64 位十六进制字符串。
具体约定:
- 方法使用
GET或POST。 - 路径包含
/api,不包含域名、?和查询字符串。路径参数需要 URL 编码时,按实际发送的编码后路径签名。 - POST 必须对最终发送的原始 JSON 字节取摘要。签名后不要再格式化、改变空格、调整字段顺序或改变中文转义方式。
- Secret 按创建时取得的字符串直接使用,不先进行 Base64 解码。
- 示例使用纯十六进制签名。当前服务端也接受
sha256=前缀,但不要求添加。 - Idempotency-Key 是单独的业务幂等头,不是 nonce,也不是上述五行之一。
3.3 当前 GET 空请求体的兼容规则
当前版本的特殊行为:不带请求体的 GET,摘要需要按 UTF-8 字符串 {} 计算。GET 本身仍不发送请求体。
固定摘要为:
44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a
这与通常“空请求体按空字节计算 SHA256”的规则不同,也是当前页面只写 SHA256(rawBody) 容易导致验签失败的原因。本节是现版本兼容说明,不是理想协议的新设计。后续修正必须由平台说明兼容策略,本稿示例按现行为编写。
3.4 Python 签名函数
下面只计算请求头,不访问网络。使用 Python 标准库即可。
import hashlib
import hmac
import uuid
import time
def make_headers(method, path, raw_body, access_key, secret,
timestamp=None, nonce=None):
# path 必须是实际发送的路径,不包含域名和 query。
if not path.startswith("/api/openapi/v1/") or "?" in path or "#" in path:
raise ValueError("请传入不含 query 的完整接口路径")
timestamp = str(int(time.time()) if timestamp is None else timestamp)
nonce = str(uuid.uuid4()) if nonce is None else nonce
# None 表示 GET 无请求体;兼容当前服务端的 {} 摘要规则。
bytes_to_hash = b"{}" if raw_body is None else raw_body
body_hash = hashlib.sha256(bytes_to_hash).hexdigest()
source = "\n".join([method.upper(), path, timestamp, nonce, body_hash])
signature = hmac.new(
secret.encode("utf-8"), source.encode("utf-8"), hashlib.sha256
).hexdigest()
return {
"X-App-Key": access_key,
"X-Timestamp": timestamp,
"X-Nonce": nonce,
"X-Signature": signature,
}
不要把函数生成的请求头或真实 Secret 完整打印到日志。
3.5 固定签名校对样例(不用于线上请求)
使用虚构 Secret doc-example-secret、时间戳 1789344000、nonce 550e8400-e29b-41d4-a716-446655440000,GET 路径 /api/openapi/v1/sms/uplinks,不发送 body。按当前兼容规则,实际签名字符串为:
GET
/api/openapi/v1/sms/uplinks
1789344000
550e8400-e29b-41d4-a716-446655440000
44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a
上面共有 4 个 LF 分隔字节,结尾没有 LF。期望签名为:
f551ad48ea2a16762b0144f0f0d6e9110c1732adc003fcb94658e5333116eb65
使用第 3.4 节函数并显式传入上述 timestamp 和 nonce,应得到完全相同的 X-Signature。这些固定值只用于离线校对;真实请求使用当前时间、新 UUID 和自己的凭据。
4. 第一个调用:查询最近的上行短信
将第 3.4 节函数和下面代码放在同一个 Python 文件。通过本机安全配置提供 CMPP_HTTP_ACCESS_KEY 和 CMPP_HTTP_SECRET 环境变量,再运行脚本。此代码只执行 GET,不发送短信。
import json
import os
import urllib.error
import urllib.parse
import urllib.request
origin = "https://api.lisglo.com"
path = "/api/openapi/v1/sms/uplinks"
query = urllib.parse.urlencode({"limit": 10})
headers = make_headers(
"GET", path, None,
os.environ["CMPP_HTTP_ACCESS_KEY"],
os.environ["CMPP_HTTP_SECRET"],
)
headers["Accept"] = "application/json"
request = urllib.request.Request(
origin + path + "?" + query, headers=headers, method="GET"
)
try:
with urllib.request.urlopen(request, timeout=15) as response:
result = json.loads(response.read().decode("utf-8"))
# 只输出计数,不在示例中打印客户手机号和短信正文。
print("HTTP", response.status)
print("本页条数:", len(result["items"]))
print("是否有下一页:", result["nextCursor"] is not None)
except urllib.error.HTTPError as error:
try:
problem = json.loads(error.read().decode("utf-8"))
except (ValueError, UnicodeError):
problem = {}
print("HTTP", error.code, "业务码:", problem.get("code", "非标准错误响应"))
except urllib.error.URLError:
print("连接失败,请检查网络、域名及 HTTPS 连通性")
没有匹配到上行数据时,正常返回:
{"items": [], "nextCursor": null}
空列表不是失败。401 优先检查签名,403 检查应用能力与白名单,429 降低请求频率。
5. 发送单条短信
5.1 请求
POST /api/openapi/v1/sms/messages
完整 cURL 请求和 HTTP 响应见第 11.2 节。
除鉴权头外,需要 Content-Type: application/json 和 Idempotency-Key。
| JSON 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
mobile |
string | 是 | 单个中国大陆手机号,例如 13800138000;不能传数组或逗号分隔号码 |
content |
string | 是 | 完整短信正文,包含签名和已填入的变量值;不能为空 |
clientMessageId |
string | 否,建议提供 | 你的业务系统消息编号,应用内唯一;按不超过 128 字符使用 |
{
"mobile": "13800138000",
"content": "【示例签名】您的验证码是123456,5分钟内有效。",
"clientMessageId": "order-20260914-0001"
}
正文由平台识别签名、模板和变量,不传 tenantId、applicationId、signatureId、templateId 或自选通道。上例签名和模板仅作说明,发送前必须使用你已审核且符合发送条件的内容。
下面代码只准备请求,不执行发送;接在第 3.4 节函数之后使用:
import json
payload = {
"mobile": "13800138000",
"content": "【示例签名】您的验证码是123456,5分钟内有效。",
"clientMessageId": "order-20260914-0001",
}
raw_body = json.dumps(
payload, ensure_ascii=False, separators=(",", ":")
).encode("utf-8")
path = "/api/openapi/v1/sms/messages"
# access_key、secret 从你的安全配置读取。
headers = make_headers("POST", path, raw_body, access_key, secret)
headers["Content-Type"] = "application/json"
headers["Idempotency-Key"] = "sms-order-20260914-0001"
# 由你的发送代码将同一个 raw_body 原样交给 HTTP 客户端。
# 不要让 HTTP 客户端再把 payload 重新序列化成另一组字节。
5.2 受理响应
成功受理返回 HTTP 202,示例:
{
"code": "ACCEPTED",
"requestId": "req_11111111-1111-4111-8111-111111111111",
"messageId": "MSG-22222222-2222-4222-8222-222222222222",
"clientMessageId": "order-20260914-0001",
"status": "queued",
"acceptedAt": "2026-09-14T02:00:00.000Z"
}
| 字段 | 含义 |
|---|---|
code |
ACCEPTED 表示接口受理成功 |
requestId |
本次业务请求编号,排查问题时提供给平台 |
messageId |
平台短信编号,用于后续查询;请持久保存 |
clientMessageId |
你传入的编号,未提供时为 null |
status |
当前短信或任务状态,不保证总是 queued,也可能需要审核 |
acceptedAt |
受理响应时间,ISO 8601;示例中的 Z 表示 UTC |
202 ≠ 已提交运营商 ≠ 手机已收到。 请用状态查询或回执判断结果;HTTP 200/202 也不能直接解释为计费成功或退款完成。若 202 缺少 messageId,保留 requestId 和 clientMessageId 联系平台核查,不因缺失就创建新发送。
5.3 重复请求与超时处理
| 情况 | 处理方式 |
|---|---|
| 网络超时,不确定平台是否受理 | 先按 clientMessageId 查询;必要时用原 Idempotency-Key 和完全相同的 body 重试请求,重新生成 timestamp/nonce/签名 |
| 相同幂等键、相同 body,原请求已完成 | 返回原受理响应,原编号和 acceptedAt 保持不变 |
| 相同幂等键、相同 body,原请求业务失败已保存 | 重放原失败,不会因稍后改好配置就自动重新发送 |
| 相同幂等键、不同 body | 409 IDEMPOTENCY_CONFLICT |
| 原请求仍处理中 | 409 REQUEST_PROCESSING;稍后查询,长期不结束联系平台 |
| 换了幂等键,但 clientMessageId 已关联短信 | 409 CLIENT_MESSAGE_ID_CONFLICT |
同一业务短信的幂等键要持久保存。JSON 字段顺序、空格变化也可能被视为不同 body。不要为了绕过 409 换键反复发送。修正确定失败的业务后是否发起新发送,由你的业务流程明确决定。
6. 查询短信状态
完整 cURL 请求和 HTTP 响应见第 11.3 节。
GET /api/openapi/v1/sms/messages/{messageId},无请求体。
路径末尾可以填写平台返回的 messageId,也支持原来提交的 clientMessageId。请对路径参数做 URL 编码;只能查询当前凭据所属应用的数据。
示例响应:
{
"messageId": "MSG-22222222-2222-4222-8222-222222222222",
"clientMessageId": "order-20260914-0001",
"phoneNumber": "13800138000",
"status": "delivered",
"submitStatus": "accepted",
"receiptStatus": "delivered",
"errorCode": null,
"errorMessage": null,
"queuedAt": "2026-09-14T02:00:00.000Z",
"submittedAt": "2026-09-14T02:00:01.000Z",
"deliveredAt": "2026-09-14T02:00:03.000Z",
"updatedAt": "2026-09-14T02:00:03.100Z"
}
| 字段 | 类型 | 说明 |
|---|---|---|
messageId |
string | 平台短信编号 |
clientMessageId |
string/null | 客户消息编号 |
phoneNumber |
string | 接收手机号;注意响应字段不是 mobile |
status |
string | 平台业务状态 |
submitStatus |
string/null | 提交状态,如 queued、accepted、rejected、timeout |
receiptStatus |
string/null | 回执状态,如 delivered、undelivered、unknown;null 表示尚无结果 |
errorCode / errorMessage |
string/null | 失败码与说明,未发生或暂无时可为空 |
queuedAt / updatedAt |
string | 入队记录时间、最近更新时间 |
submittedAt / deliveredAt |
string/null | 提交时间、结果时间,尚无数据时可为空 |
常见 status 的业务含义:
| 状态 | 你应如何理解 |
|---|---|
pending_review |
等待审核,不代表已送出 |
queued / submit_queued |
等待平台处理或向通道提交 |
submitted |
已提交,等待明确回执 |
delivered |
已获得送达结果 |
submit_failed / failed / rejected |
提交失败、发送失败或业务拒绝,结合错误字段查看原因 |
unknown / timeout |
结果未知或等待超时,不能当成送达 |
上述是常见值,不是服务端严格冻结的全部枚举。遇到其他值保留原值并展示“待确认”,不要默认成功。deliveredAt 在失败回执中也可能是结果发生时间,不能只看这个字段非空就认为送达。
不存在或不属于本应用时返回 404 MESSAGE_NOT_FOUND。查询刚超时的发送得到 404 也不能单独证明业务永远不会落库。
7. 查询手机用户回复(上行)
7.0 先分清四种时间边界
| 项目 | 当前规则 | 举例 |
|---|---|---|
| 默认查询范围 | endTime 默认现在;startTime 默认 endTime 前 24 小时 | 什么时间都不传,就是最近 24 小时 |
| 单次最大查询跨度 | 默认 31 天,以应用配置为准;限制起止时间之差 | 可查询三个月前某一天,并不是只能查询最近 31 天 |
| 历史数据保留期 | 有默认 90 天的配置,但尚未确认存在按该配置执行的保留/清理闭环 | 不能承诺一定有 90 天数据,也不能断言更早数据必定被删除 |
| 游标有效期 | 当前游标没有单独过期计时;不保证数据长期不变 | 不将旧 cursor 当作永久同步凭据,重新固定时间窗口并去重 |
查询代码没有额外强制“只查最近 N 天”,但历史查询能否返回数据取决于实际留存、当前应用归属和筛选条件。startTime 必须不晚于 endTime,当前时间边界两端均包含。跨窗口同步可按上行 id 去重,避免公共边界重复计入。
时间接受 YYYY-MM-DD(按UTC零点)或带 Z / ±HH:mm 时区的ISO 8601时间;秒可省略,小数秒最多三位。日期必须在日历上真实存在,不接受2月30日、24:00、无时区日期时间或本地化日期格式;空字符串也不是省略参数。游标中的时间执行同样校验。
7.1 列表与筛选
第一页、空数据及下一页的完整报文见第 11.4~11.5 节。
GET /api/openapi/v1/sms/uplinks,无请求体。
| Query 参数 | 类型 | 默认值/规则 |
|---|---|---|
startTime |
ISO 8601 时间字符串 | 默认 endTime 向前 24 小时 |
endTime |
ISO 8601 时间字符串 | 默认当前时间 |
mobile |
string | 可选,精确匹配回复者手机号 |
accessNumber |
string | 可选,精确匹配回复到的接入号,对应响应 destId |
keyword |
string | 可选,按回复正文包含关键词筛选 |
limit |
正整数 | 默认 50;不超过应用最大分页配置,系统默认上限 100 |
cursor |
string | 第一页不传,下一页使用上一响应 nextCursor |
单次时间跨度默认上限 31 天,以应用配置为准。建议总是带时区,例如 2026-09-14T00:00:00+08:00 或 2026-09-13T16:00:00Z;URL 中的 + 应通过标准 query 编码转换为 %2B。空筛选项直接省略,不传空字符串。limit 按上述正整数约定使用,不依赖当前不完善的非法参数处理。
示例请求 URL:
https://api.lisglo.com/api/openapi/v1/sms/uplinks?startTime=2026-09-13T16%3A00%3A00Z&endTime=2026-09-14T16%3A00%3A00Z&limit=25
签名的路径仍为 /api/openapi/v1/sms/uplinks,不包含 ? 后的内容。
示例响应:
{
"items": [
{
"id": "uplink-example-001",
"messageId": null,
"phoneNumber": "13800138000",
"destId": "106900000000",
"content": "收到,谢谢",
"receivedAt": "2026-09-14T02:05:00.000Z"
}
],
"nextCursor": null
}
id 是上行记录编号,用它查询详情;messageId 可能为空,不用它代替上行 id。destId 是接收用户回复的短信接入号。内部匹配状态与诊断不对外返回。只返回归属本应用的已匹配/已认领数据,不返回未匹配或存在归属歧义的数据。
7.2 翻页方法
- 第一页固定 startTime、endTime 和筛选条件,不传 cursor。
- 若 nextCursor 非 null,下一次保持时间、筛选和 limit 不变,只加入 cursor;仍需新的 timestamp/nonce/签名。
- nextCursor 为 null 时结束。没有总条数、页码或 totalPages。
记录按接收时间、记录 id 倒序返回。cursor 是不透明令牌,不自行解码、构造或当作长期同步水位。上行可能延迟到达或稍后才被认领;持续同步宜重叠查询时间窗口并按 id 去重,不把一次分页当作永不变化的快照。
7.3 上行详情
完整 cURL 请求和 HTTP 响应见第 11.6 节。
GET /api/openapi/v1/sms/uplinks/{uplinkId},无请求体。
把列表的 items[].id 或回调的 data.uplinkMessageId 放入路径,按第 3 节签名。返回单个上行对象,核心字段与列表中的 item 相同,不再包裹 items。
详情额外返回当前客户自己的 tenantId、applicationId。不返回通道、供应商编号、内部事件/匹配诊断、数据库关联行号等字段。本次按数据边界收紧旧输出,旧客户需移除对这些内部字段的依赖。不存在或不属于当前企业应用时返回 404 UPLINK_NOT_FOUND。
8. 平台怎样主动通知你的系统
8.1 配置与接收
在客户端“回调配置”分别设置:
- 回执回调 URL:接收
receipt,用于获知短信结果。 - 上行回调 URL:接收
uplink,用于接收用户回复。
平台向对应 URL 发起 HTTP POST。需开通该类回调能力、保存非空有效地址。使用平台可访问的公网 HTTPS 地址,不使用本机、内网或依赖重定向的地址。每个回调端点使用自己的签名密钥。
不要假设每次发送请求都会有回调。同步参数错误、未受理的请求以及未生成该类事件的分支,应处理接口响应或查询结果。长短信的 HTTP 结果通知按业务消息处理,不按运营商内部计费分片逐片回调。
8.2 请求头和事件格式
两类通知的完整 HTTP 请求及接收端响应见第 11.7 节。
| 回调头 | 说明 |
|---|---|
Content-Type |
application/json |
X-Event-Id |
稳定事件编号,用于去重 |
X-Event-Type |
receipt 或 uplink |
X-Timestamp |
本次投递的 Unix 秒级时间戳 |
X-Signature |
sha256= 加 HMAC-SHA256 十六进制值 |
回执示例:
{
"eventId": "evt_receipt_example001",
"eventType": "receipt",
"occurredAt": "2026-09-14T02:00:03.100Z",
"data": {
"messageId": "MSG-22222222-2222-4222-8222-222222222222",
"gatewayMessageId": "example-gateway-id",
"phoneNumber": "13800138000",
"receiptStatus": "delivered",
"rawStatus": "DELIVRD",
"errorCode": null,
"deliveredAt": "2026-09-14T02:00:03.000Z"
}
}
receiptStatus 表示结果,rawStatus 是原始状态,errorCode 可空,部分失败事件还含 errorMessage。网关编号只作为辅助信息,使用 messageId 与自己的发送记录关联。当前回执不保证带 clientMessageId,请保存发送响应中的两种编号映射。未提供的可选字段可能缺省,也可能为 null。
上行示例:
{
"eventId": "evt_uplink_example001",
"eventType": "uplink",
"occurredAt": "2026-09-14T02:05:00.100Z",
"data": {
"applicationId": "application-example",
"phoneNumber": "13800138000",
"destId": "106900000000",
"content": "收到,谢谢",
"receivedAt": "2026-09-14T02:05:00.000Z",
"uplinkMessageId": "uplink-example-001"
}
}
上行 data 还可能包含 messageId,人工认领的通知可含 manualClaim: true;没有 messageId 时不能强行关联一条发送记录。occurredAt 是平台创建事件的时间,与数据里的 deliveredAt/receivedAt 含义不同;重试时事件编号和事件体保持不变,投递时间戳与签名会重新生成。
8.3 如何验签
回调的签名原文是 时间戳 + 一个 LF 换行 + 原始请求体字节,使用该回调端点的签名密钥执行 HMAC-SHA256。它不是第 3 节的五行请求签名。
先保留 HTTP 原始 body 再验签,不要解析 JSON 后重新序列化。下面是纯计算示例;header 名大小写不敏感:
import hashlib
import hmac
import json
import time
def verify_callback(headers, raw_body, webhook_secret, now=None,
allowed_skew_seconds=300):
# 300 秒是本示例的接收端策略,可按双方约定调整。
hs = {str(k).lower(): str(v) for k, v in headers.items()}
timestamp = hs.get("x-timestamp", "")
if not timestamp.isdigit():
return False
now = time.time() if now is None else now
if abs(now - int(timestamp)) > allowed_skew_seconds:
return False
supplied = hs.get("x-signature", "")
if not supplied.startswith("sha256="):
return False
source = timestamp.encode("utf-8") + b"\n" + raw_body
expected = hmac.new(
webhook_secret.encode("utf-8"), source, hashlib.sha256
).hexdigest()
if not hmac.compare_digest(expected, supplied[7:]):
return False
try:
event = json.loads(raw_body)
except (ValueError, UnicodeError):
return False
if not isinstance(event, dict):
return False
return (
isinstance(event.get("eventId"), str)
and bool(event["eventId"])
and event.get("eventId") == hs.get("x-event-id")
and event.get("eventType") == hs.get("x-event-type")
and event.get("eventType") in {"receipt", "uplink"}
and isinstance(event.get("data"), dict)
)
验签后,仍需在数据库以 eventId 做唯一约束或等效的原子去重,不能只用进程内集合。对重复且已成功保存的事件正常确认;不要因重复而再次更新余额、发券或触发另一条短信。不同事件的到达先后也不能代替业务状态判断。
8.4 怎样确认接收、失败后会怎样
平台把 任意 HTTP 2xx 视为接收成功,不要求固定响应 JSON;可以返回 200 OK。建议先验签并将事件可靠保存,再尽快返回 2xx,后续业务异步处理。返回了 2xx 即使正文写“失败”,平台也按成功处理。
按设计:网络错误、408、429、5xx 可重试;其他 4xx 终结,重定向不跟随。默认总尝试上限 7 次(含首次),预期各次失败后的间隔为 1 分钟、5 分钟、15 分钟、1 小时、6 小时、24 小时,并受应用策略限制。这些是相邻尝试的等待间隔,不是统一从首次起算。默认配置超时为 10 秒,不应依赖它作为严格端到端时限。
整改后的新回调使用稳定任务编号和数据库耐久待办;Redis暂不可用时保留待办,由后续扫描恢复。每次投递按数据库租约认领,尝试结果与下一次等待时间同事务保存。网络中断可能造成重复接收,仍须按eventId幂等。历史pending/retrying不会在升级时自动补投,须另行核对处理;部署与验收完成情况见测试进度。
9. 常见错误与处理
由业务接口异常过滤器处理的错误,通常使用 application/problem+json:
{
"type": "https://cmpp-platform.local/problems/signature_invalid",
"title": "UNAUTHORIZED",
"status": 401,
"code": "SIGNATURE_INVALID",
"detail": "请求签名校验失败"
}
以 HTTP 状态和 code 做程序判断,detail 供人阅读;type 当前是标识符,不是可访问的帮助链接。代理、网络或 JSON 解析层也可能返回其他格式,应先检查 Content-Type,不要直接假设每次失败都能解析 JSON。
| HTTP | code | 处理方法 |
|---|---|---|
| 400 | PARAMETER_INVALID / LIMIT_INVALID |
检查字段类型、长度和正整数分页 |
| 409 | REQUEST_REQUIRES_REVIEW |
提供requestId核对,不更换幂等键重发 |
| 400 | MOBILE_INVALID / CONTENT_REQUIRED |
检查单个手机号和正文 |
| 400 | IDEMPOTENCY_KEY_INVALID |
补齐有效幂等键,检查长度与字符 |
| 400 | TIME_RANGE_INVALID / TIME_RANGE_TOO_LARGE |
检查时间格式、先后顺序和跨度 |
| 413 | PAYLOAD_TOO_LARGE |
JSON请求体超过2 MiB限制,减少内容后再提交 |
| 415 | UNSUPPORTED_MEDIA_TYPE |
检查请求体编码及字符集,使用UTF-8 JSON |
| 400 | CURSOR_INVALID |
使用平台返回的 cursor,保持筛选条件一致 |
| 401 | AUTH_HEADERS_MISSING |
补齐四个鉴权头 |
| 401 | CREDENTIAL_INVALID |
核对应用、Access Key、有效期和吊销状态 |
| 401 | NONCE_INVALID / NONCE_REPLAYED |
检查格式,每次调用生成新的 nonce |
| 401 | TIMESTAMP_EXPIRED |
使用秒级时间戳并同步时钟 |
| 401 | SIGNATURE_INVALID |
核对 Secret、五行原文、路径、原始 body 和 GET 兼容摘要 |
| 403 | HTTP_API_DISABLED |
应用未启用或未开通 HTTP,联系平台 |
| 403 | IP_NOT_ALLOWED |
核对调用机器公网出口 IP 和 HTTP 白名单 |
| 403 | SEND_NOT_ENABLED / MESSAGE_QUERY_NOT_ENABLED / UPLINK_QUERY_NOT_ENABLED |
联系平台开通对应子能力 |
| 404 | MESSAGE_NOT_FOUND / UPLINK_NOT_FOUND |
核对编号及凭据所属应用 |
| 409 | IDEMPOTENCY_CONFLICT |
同键 body 不一致,停止盲重试并核对原请求 |
| 409 | REQUEST_PROCESSING |
查询既有业务结果;长期停留联系平台 |
| 409 | CLIENT_MESSAGE_ID_CONFLICT |
查询已有客户编号对应记录,避免重复业务发送 |
| 422 | SEND_REJECTED |
正文、审核或其他业务条件不满足,按说明处理,不盲重试 |
| 429 | QPS_LIMIT_EXCEEDED |
应用共享限流,降低并发、延后并加随机退避;新凭据不能扩大应用额度 |
| 5xx | REQUEST_FAILED / INTERNAL_ERROR 等 |
保留请求标识和时间联系平台;发送场景先查询,避免重新创建业务 |
表格覆盖明确的常见分支,不穷尽发送链下游的所有错误。当前 5xx 文案/首次与重放错误码尚未完全统一,不依赖内部错误文本写业务逻辑。发生连接中断时可能没有任何 HTTP 状态或 JSON,按网络错误处理。
每次交互响应头 X-Request-Id 用于定位本次调用;发送受理正文中的 requestId 对应稳定业务幂等记录,两者含义不同。未知错误固定为 INTERNAL_ERROR,不返回依赖异常正文。
提交排障信息时提供:应用名称、请求时间与时区、接口路径、HTTP 状态、业务 code、requestId/messageId/clientMessageId 或 eventId。手机号和正文按需要脱敏,不提供 Secret 或完整鉴权头。
10. 当前版本的接入边界
| 事项 | 当前说明 |
|---|---|
| Swagger 准确性 | 查询响应/上行参数未补全,Idempotency-Key 重复、User-Agent 必填及 clientMessageId 类型存在错误;以本文明确的现状说明对照实现 |
| GET 空体 | 当前须使用 {} 摘要;后续变更需确认兼容策略 |
| 回调重试 | 新事件耐久待办和安全任务编号;真实验收与部署状态单独记录,历史事件不自动重投 |
| 超时与幂等恢复 | 受理快照与消息/待办同事务;结果无法确定时返回 REQUEST_REQUIRES_REVIEW,不能换幂等键绕过 |
| 参数与响应模型 | clientMessageId为string/null且最长128;limit为正整数,按应用上限裁剪;上行不返回内部通道和匹配字段 |
| 性能 | 以应用配置限制请求频率;QPS 配置不是对全链路送达能力的承诺 |
| 文档验证 | 本稿完成源码对照、公开契约读取和离线示例校验;没有使用客户凭据发送或完成真实回调验收 |
11. 逐接口 cURL 与 HTTP 报文示例
11.1 公共准备与占位符
本节每个接口均提供 cURL 请求、成功和常见失败 HTTP 响应;回调提供平台 POST 和客户 ACK。为便于阅读,HTTP 报文省略 Content-Length、Date、连接等由服务器或 HTTP 客户端生成的头;不要把签名原文的 LF 规则套到 HTTP 报文行结束符上。
cURL 使用 Bash 语法(PowerShell 应使用 curl.exe 并调整续行/变量语法)。$ACCESS_KEY、$TIMESTAMP、$NONCE、$SIGNATURE 分别对应第 3.4 节函数返回的四个头;$CURSOR 必须取上一页真实 nextCursor。这些不是固定测试凭据。下面的可视 cURL 不能在未生成动态鉴权值时直接调用;固定签名只用于第 3.5 节离线校对。
下面的公共辅助函数引用第 3.4 节 make_headers,直接生成带动态签名的 cURL 参数和 stdin 字节,免去手工设置上述变量。将它与签名函数放在同一 Python 文件,从安全环境变量读取凭据。函数本身不访问网络;不要打印返回的完整鉴权参数。
import os
from urllib.parse import urlencode
def prepare_curl(method, path, raw_body=None, query=None, idempotency_key=None):
method = method.upper()
if method == "POST" and (raw_body is None or not idempotency_key):
raise ValueError("发送需提供最终原始字节和稳定幂等键")
headers = make_headers(method, path, raw_body,
os.environ["CMPP_HTTP_ACCESS_KEY"],
os.environ["CMPP_HTTP_SECRET"])
url = "https://api.lisglo.com" + path
if query:
url += "?" + urlencode(query)
args = ["curl", "--include", "--max-time", "15", "--request", method, url]
for key, value in headers.items():
args.extend(["--header", f"{key}: {value}"])
if raw_body is not None:
args.extend(["--header", "Content-Type: application/json",
"--data-binary", "@-"])
if idempotency_key:
args.extend(["--header", f"Idempotency-Key: {idempotency_key}"])
return args, raw_body
例如 args, body = prepare_curl("GET", "/api/openapi/v1/sms/uplinks", query={"limit": 10}) 只准备查询。由客户在确认环境后使用 subprocess.run(args, input=body, check=True) 实际调用;HTTP 状态仍须单独判断,cURL 进程退出 0 不代表业务成功。POST 将完整 JSON 字节通过 stdin 传入,避免重新序列化;本稿没有自动执行发送的入口。
11.2 单条发送
将第 5.1 节 JSON 保存为 UTF-8 无 BOM 的 sms.json,签名时读取该文件的原始字节,随后原样发送。此命令会真实提交短信,示例不自动执行。
curl --include --max-time 15 --request POST \
"https://api.lisglo.com/api/openapi/v1/sms/messages" \
--header "X-App-Key: $ACCESS_KEY" \
--header "X-Timestamp: $TIMESTAMP" \
--header "X-Nonce: $NONCE" \
--header "X-Signature: $SIGNATURE" \
--header "Content-Type: application/json" \
--header "Idempotency-Key: sms-order-20260914-0001" \
--data-binary "@sms.json"
成功响应示例:
HTTP/1.1 202 Accepted
Content-Type: application/json; charset=utf-8
{
"code": "ACCEPTED",
"requestId": "req_11111111-1111-4111-8111-111111111111",
"messageId": "MSG-22222222-2222-4222-8222-222222222222",
"clientMessageId": "order-20260914-0001",
"status": "queued",
"acceptedAt": "2026-09-14T02:00:00.000Z"
}
失败响应示例:
HTTP/1.1 409 Conflict
Content-Type: application/problem+json; charset=utf-8
{
"type": "https://cmpp-platform.local/problems/idempotency_conflict",
"title": "CONFLICT",
"status": 409,
"code": "IDEMPOTENCY_CONFLICT",
"detail": "同一Idempotency-Key对应的请求内容不一致"
}
11.2.1 完整生成脚本:不再手工填写请求头变量
上面的 $NONCE、$TIMESTAMP 等是 Bash 变量引用,不是可以原样发送的参数值。未设置变量时,单独复制上面的结构示例不能正常鉴权。
下面提供完整脚本:实际生成 UUID v4、当前时间戳、请求体摘要和 HMAC-SHA256 签名,再输出没有请求头变量占位符的 Bash cURL 命令。保存为 UTF-8 编码的 generate_sms_curl.py,运行 python3 generate_sms_curl.py。
脚本只生成并打印命令,不访问接口、不发送短信。UUID 和签名是真实生成、真实计算的;Access Key 和 Secret 明确为未开户的演示凭据,不能据此鉴权成功。
import hashlib
import hmac
import json
import shlex
import time
import uuid
# 演示凭据,不是真实客户账号。
ACCESS_KEY = "DEMO_ACCESS_KEY_NOT_REGISTERED"
SECRET = "DEMO_SECRET_NOT_A_REAL_CREDENTIAL"
origin = "https://api.lisglo.com"
path = "/api/openapi/v1/sms/messages"
# 同一次业务发送重试时,两个业务编号和请求体保持不变。
idempotency_key = "sms-doc-example-20260914-0001"
payload = {
"mobile": "13800138000",
"content": "【示例签名】您的验证码是123456,5分钟内有效。",
"clientMessageId": "doc-example-20260914-0001",
}
# 只序列化一次:计算摘要与最终发送使用同一份内容。
body = json.dumps(payload, ensure_ascii=False, separators=(",", ":"))
raw_body = body.encode("utf-8")
timestamp = str(int(time.time()))
nonce = str(uuid.uuid4())
body_hash = hashlib.sha256(raw_body).hexdigest()
signing_string = "\n".join([
"POST", path, timestamp, nonce, body_hash,
])
signature = hmac.new(
SECRET.encode("utf-8"),
signing_string.encode("utf-8"),
hashlib.sha256,
).hexdigest()
headers = {
"Content-Type": "application/json",
"X-App-Key": ACCESS_KEY,
"X-Timestamp": timestamp,
"X-Nonce": nonce,
"X-Signature": signature,
"Idempotency-Key": idempotency_key,
}
lines = [
"curl --include --max-time 15 --request POST "
+ shlex.quote(origin + path)
]
for name, value in headers.items():
lines.append("--header " + shlex.quote(f"{name}: {value}"))
lines.append("--data-binary " + shlex.quote(body))
print("本次实际生成的 UUID:", nonce)
print("本次时间戳:", timestamp)
print("请求体 SHA256:", body_hash)
print("计算得到的签名:", signature)
print("\n完整 cURL 命令(仅生成,未执行):\n")
print(" \\\n ".join(lines))
该脚本每次运行都会产生新的 nonce 和时间戳,因而签名也会变化。输出命令按 Bash 和 UTF-8 编写,不应直接当作 PowerShell 语法执行。脚本用于演示,会打印鉴权头;正式接入时凭据从安全配置读取,不把完整鉴权信息写入共享日志。
11.2.2 实际计算的完整 cURL 示例
以下为2026-09-14实际生成的一组结果,不是 $NONCE 或 $SIGNATURE 占位符,也不是成功发送记录:
| 项目 | 本次实际使用或计算的值 |
|---|---|
| 演示 Access Key | DEMO_ACCESS_KEY_NOT_REGISTERED(未开户) |
| 演示 Secret | DEMO_SECRET_NOT_A_REAL_CREDENTIAL(无真实账号权限) |
| Unix 秒级时间戳 | 1789355443 |
| 实际生成的 UUID v4 | 7921b5d1-3b99-48d4-a068-ea7cf0c998db |
| 原始请求体 SHA256 | c6cd931342e983778342e2175c0c60eafc87f28c892378761d0e6f7065728a29 |
| 实际计算的 HMAC-SHA256 | 90cd7c99308406868fcdad059a9e61aef0a5d1275756885c4e277aa20e787a4b |
curl --include --max-time 15 --request POST \
'https://api.lisglo.com/api/openapi/v1/sms/messages' \
--header 'Content-Type: application/json' \
--header 'X-App-Key: DEMO_ACCESS_KEY_NOT_REGISTERED' \
--header 'X-Timestamp: 1789355443' \
--header 'X-Nonce: 7921b5d1-3b99-48d4-a068-ea7cf0c998db' \
--header 'X-Signature: 90cd7c99308406868fcdad059a9e61aef0a5d1275756885c4e277aa20e787a4b' \
--header 'Idempotency-Key: sms-doc-example-20260914-0001' \
--data-binary '{"mobile":"13800138000","content":"【示例签名】您的验证码是123456,5分钟内有效。","clientMessageId":"doc-example-20260914-0001"}'
真实的是 UUID 生成过程、摘要和签名计算结果,不是客户凭据或业务调用成功。 固定示例可离线复算,但演示凭据无效,时间戳也会过期。本次未执行这条命令。使用真实凭据时,必须重新生成时间戳、nonce 和签名,不能只替换 Access Key;执行有效的发送命令可能真正创建短信任务并产生费用。
11.3 短信状态
curl --include --max-time 15 --request GET \
"https://api.lisglo.com/api/openapi/v1/sms/messages/MSG-22222222-2222-4222-8222-222222222222" \
--header "X-App-Key: $ACCESS_KEY" \
--header "X-Timestamp: $TIMESTAMP" \
--header "X-Nonce: $NONCE" \
--header "X-Signature: $SIGNATURE"
成功响应示例:
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
{
"messageId": "MSG-22222222-2222-4222-8222-222222222222",
"clientMessageId": "order-20260914-0001",
"phoneNumber": "13800138000",
"status": "delivered",
"submitStatus": "accepted",
"receiptStatus": "delivered",
"errorCode": null,
"errorMessage": null,
"queuedAt": "2026-09-14T02:00:00.000Z",
"submittedAt": "2026-09-14T02:00:01.000Z",
"deliveredAt": "2026-09-14T02:00:03.000Z",
"updatedAt": "2026-09-14T02:00:03.100Z"
}
失败响应示例:
HTTP/1.1 404 Not Found
Content-Type: application/problem+json; charset=utf-8
{
"type": "https://cmpp-platform.local/problems/message_not_found",
"title": "NOT_FOUND",
"status": 404,
"code": "MESSAGE_NOT_FOUND",
"detail": "短信记录不存在"
}
11.4 上行列表第一页
curl --include --max-time 15 --request GET \
"https://api.lisglo.com/api/openapi/v1/sms/uplinks" --get \
--data-urlencode "startTime=2026-09-13T16:00:00Z" \
--data-urlencode "endTime=2026-09-14T16:00:00Z" \
--data-urlencode "limit=25" \
--header "X-App-Key: $ACCESS_KEY" \
--header "X-Timestamp: $TIMESTAMP" \
--header "X-Nonce: $NONCE" \
--header "X-Signature: $SIGNATURE"
成功响应示例:
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
{
"items": [
{
"id": "uplink-example-001",
"messageId": null,
"phoneNumber": "13800138000",
"destId": "106900000000",
"content": "收到,谢谢",
"receivedAt": "2026-09-14T02:05:00.000Z"
}
],
"nextCursor": null
}
失败响应示例:
HTTP/1.1 400 Bad Request
Content-Type: application/problem+json; charset=utf-8
{
"type": "https://cmpp-platform.local/problems/time_range_too_large",
"title": "BAD_REQUEST",
"status": 400,
"code": "TIME_RANGE_TOO_LARGE",
"detail": "单次查询不能超过31天"
}
空列表响应:
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
{
"items": [],
"nextCursor": null
}
11.5 上行下一页
第 11.4 节只有一条记录的示例已结束,所以 nextCursor 为 null,不应继续请求。以下表示另一个有两条记录的分页场景,第一页和下一页均使用 limit=1。第一页省略 cursor,其他时间条件与第 11.4 节相同。将下面 nextCursor 完整保存到 $CURSOR;这是由虚构记录生成的格式示例,线上必须用真实上一响应的值。
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
{
"items": [
{
"id": "uplink-example-001",
"messageId": null,
"phoneNumber": "13800138000",
"destId": "106900000000",
"content": "收到,谢谢",
"receivedAt": "2026-09-14T02:05:00.000Z"
}
],
"nextCursor": "WyIyMDI2LTA5LTE0VDAyOjA1OjAwLjAwMFoiLCJ1cGxpbmstZXhhbXBsZS0wMDEiXQ"
}
保持第一页时间和筛选不变,生成新的请求唯一标识、时间戳和签名:
curl --include --max-time 15 --get \
"https://api.lisglo.com/api/openapi/v1/sms/uplinks" \
--data-urlencode "startTime=2026-09-13T16:00:00Z" \
--data-urlencode "endTime=2026-09-14T16:00:00Z" \
--data-urlencode "limit=1" \
--data-urlencode "cursor=$CURSOR" \
--header "X-App-Key: $ACCESS_KEY" \
--header "X-Timestamp: $TIMESTAMP" \
--header "X-Nonce: $NONCE" \
--header "X-Signature: $SIGNATURE"
下一页返回较早的第二条记录,nextCursor 为 null,表示没有更多页:
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
{
"items": [
{
"id": "uplink-example-000",
"messageId": null,
"phoneNumber": "13800138000",
"destId": "106900000000",
"content": "好的",
"receivedAt": "2026-09-14T02:04:00.000Z"
}
],
"nextCursor": null
}
非法游标示例:
HTTP/1.1 400 Bad Request
Content-Type: application/problem+json; charset=utf-8
{
"type": "https://cmpp-platform.local/problems/cursor_invalid",
"title": "BAD_REQUEST",
"status": 400,
"code": "CURSOR_INVALID",
"detail": "cursor格式非法"
}
11.6 上行详情
curl --include --max-time 15 --request GET \
"https://api.lisglo.com/api/openapi/v1/sms/uplinks/uplink-example-001" \
--header "X-App-Key: $ACCESS_KEY" \
--header "X-Timestamp: $TIMESTAMP" \
--header "X-Nonce: $NONCE" \
--header "X-Signature: $SIGNATURE"
成功响应示例:
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
{
"id": "uplink-example-001",
"messageId": null,
"phoneNumber": "13800138000",
"destId": "106900000000",
"content": "收到,谢谢",
"receivedAt": "2026-09-14T02:05:00.000Z",
"tenantId": "tenant-example",
"applicationId": "application-example"
}
失败响应示例:
HTTP/1.1 404 Not Found
Content-Type: application/problem+json; charset=utf-8
{
"type": "https://cmpp-platform.local/problems/uplink_not_found",
"title": "NOT_FOUND",
"status": 404,
"code": "UPLINK_NOT_FOUND",
"detail": "上行记录不存在"
}
11.7 两类回调的完整交互
以下签名位置为说明占位符,必须用第 8.3 节规则计算,不是有效固定签名。平台投递 URL 是客户配置的地址。
回执通知:
POST /webhooks/sms/receipt HTTP/1.1
Host: customer.example.com
Content-Type: application/json
X-Event-Id: evt_receipt_example001
X-Event-Type: receipt
X-Timestamp: 1789344000
X-Signature: sha256=<本次原始请求体的有效签名>
{
"eventId": "evt_receipt_example001",
"eventType": "receipt",
"occurredAt": "2026-09-14T02:00:03.100Z",
"data": {
"messageId": "MSG-22222222-2222-4222-8222-222222222222",
"gatewayMessageId": "example-gateway-id",
"phoneNumber": "13800138000",
"receiptStatus": "delivered",
"rawStatus": "DELIVRD",
"errorCode": null,
"deliveredAt": "2026-09-14T02:00:03.000Z"
}
}
上行通知:
POST /webhooks/sms/uplink HTTP/1.1
Host: customer.example.com
Content-Type: application/json
X-Event-Id: evt_uplink_example001
X-Event-Type: uplink
X-Timestamp: 1789344000
X-Signature: sha256=<本次原始请求体的有效签名>
{
"eventId": "evt_uplink_example001",
"eventType": "uplink",
"occurredAt": "2026-09-14T02:05:00.100Z",
"data": {
"applicationId": "application-example",
"phoneNumber": "13800138000",
"destId": "106900000000",
"content": "收到,谢谢",
"receivedAt": "2026-09-14T02:05:00.000Z",
"uplinkMessageId": "uplink-example-001"
}
}
两类通知均在验签并可靠保存后确认:
HTTP/1.1 200 OK
Content-Type: text/plain; charset=utf-8
OK
若接收端暂时无法可靠保存,可返回如下失败(正文由客户自定义,不是平台统一错误格式):
HTTP/1.1 503 Service Unavailable
Content-Type: text/plain; charset=utf-8
temporarily unavailable
预期会进入可重试分支,但当前自动重试缺陷仍未修复,不能仅凭返回503就假设平台一定补投。验签失败可返回401/403,这类响应通常不自动重试;应排查密钥、时间戳和原始字节。
11.8 公共鉴权失败
四个接口都可能在进入业务处理前返回以下响应:
HTTP/1.1 401 Unauthorized
Content-Type: application/problem+json; charset=utf-8
{
"type": "https://cmpp-platform.local/problems/signature_invalid",
"title": "UNAUTHORIZED",
"status": 401,
"code": "SIGNATURE_INVALID",
"detail": "请求签名校验失败"
}
其他公共失败见第 9 节。返回体的业务码和说明用于展示当前常见分支;真实网关错误可能不是该格式。
12. 版本与兼容说明
本次保持v1四个业务路径、五行LF签名、无体GET兼容摘要和请求唯一标识规则。clientMessageId必须为字符串或null、最长128字符;limit必须为正整数。上行列表与详情只输出公共业务字段,详情可含所属企业和应用ID,不再输出通道或内部匹配信息。
回调是至少一次投递,请按eventId去重。查询和回调的字段、类型以当前环境提供的OpenAPI JSON为准;接入前完成签名校对,并保存requestId和messageId以便排障。