Files
lislgosms/docs/client-http-api-guide.md
T

952 lines
44 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 聆界短信平台 HTTP API 参考手册
**接口版本:v1 · 文档日期:2026-09-15**
## 1. API 概览
聆界短信平台 HTTP API 提供单条短信发送、发送结果查询和用户回复查询。平台还可将短信回执和用户回复主动推送到客户系统。
### 1.1 服务地址与数据格式
| 项目 | 约定 |
| ------------ | ------------------------------------------- |
| 接口基础地址 | https://api.lisglo.com/api/openapi/v1 |
| OpenAPI 定义 | https://api.lisglo.com/api/client-docs-json |
| 数据格式 | UTF-8 JSON |
| 发送方式 | POST |
| 查询方式 | GET,不携带请求体 |
| 授权方式 | Access Key + HMAC-SHA256 请求签名 |
| 发送受理 | HTTP 202 |
| 查询成功 | HTTP 200 |
业务接口的成功响应直接返回结果对象;Webhook 正文包含事件信息和 data 业务对象。
### 1.2 接口目录
| 接口名称 | 方法 | 接口路径 | 成功响应 |
| -------------------- | ---- | ---------------------------------------- | ----------------- |
| 单条短信发送接口 | 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:返回指定回复 |
上表路径应拼接在 https://api.lisglo.com 后。收到 202 表示平台受理,不代表手机已经收到短信。最终结果通过回执查询或回执回调获取。
### 1.3 接入准备
1. 联系聆界平台客服创建企业账号和短信应用,开通 HTTP 及相应发送、查询、回调能力,配置调用服务器的 IP 白名单。
2. 在客户端“接口对接”选择应用,确认接口地址、QPS 和白名单。
3. 在“访问凭据”创建 Access Key 和 Secret。Secret 仅在创建或轮换时展示一次,请保存在后端安全配置中。
4. 发送前完成短信签名和模板审核;提交的完整正文应符合本应用的审核与发送要求。
5. 如需接收回调,在“回调配置”保存公网 HTTPS 地址和各自的回调签名密钥。
接口凭据只能用于所属应用,平台登录密码不能代替 Secret。回调验签密钥与接口 Secret 分开保存。轮换请求凭据时先建立新凭据并切换调用,再吊销旧凭据;回调密钥轮换需同步更新接收端。
建议先按第 2 节校对签名,再调用上行列表查询接口验证接入;没有回复时返回空列表。
### 1.4 怎样使用后面的 cURL 示例
HTTP 报文使用演示 Access Key、演示 Secret `DEMO_SECRET_NOT_A_REAL_CREDENTIAL` 和固定时间戳;签名已计算,POST 正文末尾不含换行,游标为格式示例。实际调用使用本应用凭据、当前时间、新 nonce 和上一页返回的游标。
示例中的变量按下表准备;它们表示你自己的调用参数,不是平台提供的固定凭据。
| 变量 | 准备方法 |
| ----------- | ---------------------------------------------------------- |
| $ACCESS_KEY | 填入本应用的 Access Key |
| $TIMESTAMP | 生成当前 Unix 秒级时间戳 |
| $NONCE | 为本次调用生成新的 UUID v4 |
| $SIGNATURE | 按第 2 节规则,使用本次方法、路径、时间戳、nonce 和请求体计算 |
| $CURSOR | 仅在翻页时使用,完整取自上一响应的 nextCursor |
先在本机 Bash 会话中设置相应变量,再执行 cURL。每次调用重新计算,不可直接沿用另一接口或上一次调用的签名。示例使用 Bash 续行语法;PowerShell 请使用 curl.exe 并调整变量及续行写法。
POST 示例使用 --data-binary 原样发送文件;保存 sms.json 时采用 UTF-8 无 BOM,签名也读取该文件的同一份字节。cURL 进程成功退出后仍需检查 HTTP 状态和响应。
## 2. 授权与请求签名
### 2.1 填写请求头
| 请求头 | 必填 | 填写方式 |
| ----------------- | ---------- | --------------------------------------------------------------------------------------------------------------------------------- |
| `X-App-Key` | 是 | 该应用的 Access Key |
| `X-Timestamp` | 是 | 当前 Unix 时间戳,单位为秒,例如 `1789344000`;不要传毫秒 |
| `X-Nonce` | 是 | 本次 HTTP 请求的唯一标识,推荐 UUID v4,例如 `550e8400-e29b-41d4-a716-446655440000`;同一次发送的网络重试时,需要重新生成 UUID v4 |
| `X-Signature` | 是 | 按下文计算的 HMAC-SHA256,小写十六进制字符串 |
| `Idempotency-Key` | 发送时必填 | 一次业务发送的稳定编号,8~128 字符,允许字母、数字、`.``_``:``-` |
| `Content-Type` | POST 使用 | `application/json` |
| `User-Agent` | 否 | 可填写客户端名称和版本,便于排错 |
每次调用,包括网络重试,都使用当前秒级时间戳和新的请求唯一标识。默认允许服务器时间前后相差 300 秒,具体以应用配置为准。X-Nonce 推荐 UUID v4,也接受字母、数字、下划线、短横线组成的 8~128 字符。
### 2.2 POST 发送请求如何计算签名
将请求方法、路径、时间戳、nonce 和原始请求体按以下规则拼接,使用 Secret 计算一次 HMAC-SHA256
```text
签名原文 = 字节拼接(UTF8(请求方法 + LF + 请求路径 + LF + 时间戳 + LF + nonce + LF), 原始请求体字节)
X-Signature = HEX(HMAC_SHA256(UTF8(Secret), 签名原文))
```
| 内容 | 规则 |
| --- | --- |
| 请求方法 | 大写,例如 POST |
| 请求路径 | 从 /api 开始,不含域名及问号后的查询参数;路径参数使用实际发送的 URL 编码形式 |
| 时间戳 | 与 X-Timestamp 完全一致的秒级时间戳 |
| nonce | 与 X-Nonce 完全一致 |
| LF | 一个换行字节(0x0A),不能替换成 CRLF |
| 原始请求体 | 实际发送的 UTF-8 JSON 字节,不预先计算摘要 |
| Secret | 直接使用 UTF-8 字节作为密钥,不做 Base64 解码 |
| HEX | 转为 64 位小写十六进制字符串 |
前四项后各接一个 LF,再接完整请求体。请求体可以有多行,空格、换行和字段顺序必须与实际发送内容完全一致;不要在签名时额外增加换行。Idempotency-Key 和 URL 查询参数不参与签名。
**POST 签名校对示例:**
演示 SecretDEMO_SECRET_NOT_A_REAL_CREDENTIAL。请求体为下面这一整行,开头没有 BOM,末尾没有换行:
```text
{"mobile":"13800138000","content":"【示例签名】您的验证码是1234565分钟内有效。","clientMessageId":"doc-example-20260914-0001"}
```
完整签名原文:
```text
POST
/api/openapi/v1/sms/messages
1789355443
7921b5d1-3b99-48d4-a068-ea7cf0c998db
{"mobile":"13800138000","content":"【示例签名】您的验证码是1234565分钟内有效。","clientMessageId":"doc-example-20260914-0001"}
```
**计算方法(Node.js):**
将以下代码保存为 post-signature.cjs,运行 `node post-signature.cjs`。代码使用 Node.js 自带的加密库,只进行本地计算。
```javascript
const { createHmac } = require('node:crypto');
const secret = 'DEMO_SECRET_NOT_A_REAL_CREDENTIAL';
const method = 'POST';
const path = '/api/openapi/v1/sms/messages';
const timestamp = '1789355443';
const nonce = '7921b5d1-3b99-48d4-a068-ea7cf0c998db';
const body = Buffer.from(
'{"mobile":"13800138000","content":"【示例签名】您的验证码是1234565分钟内有效。","clientMessageId":"doc-example-20260914-0001"}',
'utf8'
);
// 前四项后各接一个 LF,再拼接实际发送的请求体字节。
const prefix = method + '\n'
+ path + '\n'
+ timestamp + '\n'
+ nonce + '\n';
const signingBytes = Buffer.concat([
Buffer.from(prefix, 'utf8'),
body,
]);
const signature = createHmac('sha256', Buffer.from(secret, 'utf8'))
.update(signingBytes)
.digest('hex');
console.log(signature);
```
实际从文件发送时,将 body 的赋值改为 `const body = require('node:fs').readFileSync('sms.json');`,签名和发送使用同一份文件字节,不要解析后重新生成 JSON。
**输出结果,填入 X-Signature**
```text
a951451624d37d3e9df24045dc65d94557551dcbeada26988e25b6d49e945124
```
### 2.3 GET 查询请求如何计算签名
GET 请求不携带正文。签名只拼接请求方法、请求路径、时间戳和 nonce 四项,相邻项之间用一个 LF 换行符分隔,nonce 后不加换行。
**计算公式:**
```text
签名原文 = 请求方法 + LF + 请求路径 + LF + 时间戳 + LF + nonce
X-Signature = HEX(HMAC_SHA256(UTF8(Secret), UTF8(签名原文)))
```
UTF8 表示将字符串转换为 UTF-8 字节;HMAC_SHA256 表示以 Secret 为密钥对签名原文进行运算;HEX 表示将运算结果转换为 64 位小写十六进制字符串。
**本例输入:**
| 项目 | 值 |
| --- | --- |
| Secret | doc-example-secret |
| 请求方法 | GET |
| 请求路径 | /api/openapi/v1/sms/uplinks |
| 时间戳 | 1789344000 |
| nonce | 550e8400-e29b-41d4-a716-446655440000 |
请求路径不含域名和问号后的查询参数;包含路径参数时,使用实际发送的 URL 编码形式。时间戳和 nonce 必须分别与 X-Timestamp、X-Nonce 一致。Secret 直接作为 UTF-8 密钥使用,不做 Base64 解码;Access Key 和 Idempotency-Key 不参与本公式。
**拼接签名原文:**
```text
GET\n/api/openapi/v1/sms/uplinks\n1789344000\n550e8400-e29b-41d4-a716-446655440000
```
上面每个 \n 表示一个 LF 换行字节(0x0A),不是反斜杠和字母 n,也不能替换为 CRLF。签名原文在 nonce 的最后一个字符处结束,不追加 LF、{} 或其他正文。
**计算方法(Node.js):**
将以下代码保存为 get-signature.cjs,运行 `node get-signature.cjs`。代码使用 Node.js 自带的加密库,只进行本地计算。
```javascript
const { createHmac } = require('node:crypto');
const secret = 'doc-example-secret';
const method = 'GET';
const path = '/api/openapi/v1/sms/uplinks';
const timestamp = '1789344000';
const nonce = '550e8400-e29b-41d4-a716-446655440000';
// 四项之间用 LF 分隔,nonce 后不加 LF。
const signingText = method + '\n'
+ path + '\n'
+ timestamp + '\n'
+ nonce;
const signature = createHmac('sha256', Buffer.from(secret, 'utf8'))
.update(Buffer.from(signingText, 'utf8'))
.digest('hex');
console.log(signature);
```
**输出结果:**
```text
3db9c015c2b1c5365a0ef296a79b419653b0087daed2792cdb5717b0802eec51
```
将该结果填入 X-Signature 请求头。其他语言按相同输入字节调用标准库的 HMAC-SHA256,并输出小写十六进制,结果应一致。
本例的固定时间戳和演示密钥仅用于离线校对。实际调用使用本应用 Secret、当前秒级时间戳和新的 nonce,并重新计算签名。
## 3. API:单条短信发送
### 3.1 接口说明
向一个中国大陆手机号发送一条短信。正文填写完整短信签名及已替换变量的内容,平台据此匹配本应用的签名与模板。
| 项目 | 内容 |
| ------------ | -------------------------------------------------- |
| URL | https://api.lisglo.com/api/openapi/v1/sms/messages |
| HTTP 方法 | POST |
| Content-Type | application/json |
| 是否鉴权 | 是,四个鉴权头见第 2 节 |
| 业务幂等头 | Idempotency-Key,必填 |
| 所需能力 | 短信发送 |
| 成功状态 | 202 Accepted |
### 3.2 请求参数
| JSON 字段 | 类型 | 必填 | 说明 |
| ----------------- | ------ | ------------ | ---------------------------------------------------------------- |
| `mobile` | string | 是 | 单个中国大陆手机号,例如 `13800138000`;不能传数组或逗号分隔号码 |
| `content` | string | 是 | 完整短信正文,包含签名和已填入的变量值;不能为空 |
| `clientMessageId` | string | 否,建议提供 | 你的业务系统消息编号,应用内唯一;按不超过 128 字符使用 |
只传上述业务字段,不传企业、应用、签名、模板或通道选择字段。Idempotency-Key 为稳定的业务发送编号,8~128 字符,可使用字母、数字、点、下划线、冒号和短横线。
**请求体示例:**
```json
{
"mobile": "13800138000",
"content": "【示例签名】您的验证码是123456,5分钟内有效。",
"clientMessageId": "order-20260914-0001"
}
```
### 3.3 请求示例
将上面的 JSON 保存为 UTF-8 无 BOM 的 sms.json。按第 2 节对文件原始字节计算签名,并准备相应变量。发送命令:
```bash
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 请求报文:**
```http
POST /api/openapi/v1/sms/messages HTTP/1.1
Host: api.lisglo.com
Accept: */*
X-App-Key: DEMO_ACCESS_KEY_NOT_REGISTERED
X-Timestamp: 1789355443
X-Nonce: 4755e099-ba9a-4e6f-affa-3630261e7feb
X-Signature: e2a70fdd364cc865e251d475cec722bb949500d255649d215b0039de89f68125
Content-Type: application/json
Idempotency-Key: sms-order-20260914-0001
Content-Length: 154
{
"mobile": "13800138000",
"content": "【示例签名】您的验证码是123456,5分钟内有效。",
"clientMessageId": "order-20260914-0001"
}
```
执行使用有效凭据的发送命令会提交短信请求;文档样例不自动执行。
### 3.4 返回结果
**受理成功:**
```http
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"
}
```
| 字段 | 类型 | 说明 |
| --------------- | ----------- | ------------------------------------------- |
| code | string | ACCEPTED 表示受理成功 |
| requestId | string | 业务请求编号,用于排障 |
| messageId | string | 平台短信编号,用于查询 |
| clientMessageId | string/null | 调用方传入的业务短信编号 |
| status | string | 当前业务状态,例如 queued 或 pending_review |
| acceptedAt | string | 受理时间,ISO 8601 |
保存 messageId 与客户业务编号的对应关系。202 不等于短信送达;若未取得明确结果,先查询已有记录。
**幂等键与正文不一致:**
```http
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对应的请求内容不一致"
}
```
### 3.5 超时、重试与重复请求
| 情况 | 建议处理 |
| ---------------------------- | ----------------------------------------------------------------- |
| 网络超时,无法确认受理结果 | 先按 clientMessageId 查询;必要时使用相同幂等键和相同原始正文重试 |
| 重试一次 HTTP 调用 | 重新生成时间戳、nonce 和签名;业务幂等键及正文保持不变 |
| 相同幂等键且正文一致 | 已完成时重放原受理响应或已保存的业务失败结果 |
| 相同幂等键但正文不同 | 返回 IDEMPOTENCY_CONFLICT |
| 原请求仍处理中 | 返回 REQUEST_PROCESSING;稍后查询,长期不结束联系平台 |
| 换幂等键但客户短信编号已使用 | 返回 CLIENT_MESSAGE_ID_CONFLICT,核对已有业务 |
保存业务幂等键,不通过换键绕过冲突。JSON 空格或字段顺序变化也会被判定为不同正文。
## 4. API:单条短信回执查询
### 4.1 接口说明
查询一条短信的提交状态、回执结果和时间。
| 项目 | 内容 |
| --------- | -------------------------------------------------------------- |
| URL | https://api.lisglo.com/api/openapi/v1/sms/messages/{messageId} |
| HTTP 方法 | GET |
| 请求体 | 无 |
| 是否鉴权 | 是,按第 2 节 GET 规则签名 |
| 所需能力 | 短信状态查询 |
| 成功状态 | 200 OK |
### 4.2 路径参数
| 参数 | 类型 | 必填 | 说明 |
| --------- | ------ | ---- | -------------------------------------------------------------- |
| messageId | string | 是 | 平台短信编号,也支持发送时提交的 clientMessageId;仅限当前应用 |
路径参数需 URL 编码,签名使用编码后的实际路径。
### 4.3 请求示例
```bash
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 请求报文:**
```http
GET /api/openapi/v1/sms/messages/MSG-22222222-2222-4222-8222-222222222222 HTTP/1.1
Host: api.lisglo.com
Accept: */*
X-App-Key: DEMO_ACCESS_KEY_NOT_REGISTERED
X-Timestamp: 1789355443
X-Nonce: 3efb63ee-b99d-47c2-aa87-cbfeb7730a30
X-Signature: 8843aa140566a16a9fddd81339235a94abc218af125963ca2df365e5454f8212
```
### 4.4 返回结果
**查询成功:**
```http
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"
}
```
| 字段 | 类型 | 说明 |
| ----------------------------- | ----------- | --------------------------------------------------------------- |
| `messageId` | string | 平台短信编号 |
| `clientMessageId` | string/null | 客户消息编号 |
| `phoneNumber` | string | 接收手机号;注意响应字段不是 mobile |
| `status` | string | 平台业务状态 |
| `submitStatus` | string/null | 提交状态,如 queued、accepted、rejected、timeout |
| `receiptStatus` | string/null | 回执状态,如 delivered、undelivered、unknownnull 表示尚无结果 |
| `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 也不能单独证明业务永远不会落库。
**记录不存在或不属于当前应用:**
```http
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": "短信记录不存在"
}
```
## 5. API:上行短信列表查询
### 5.1 接口说明
查询手机用户回复的短信。无需填写时间参数,默认读取最近 24 小时内已归属本应用的回复。
| 项目 | 内容 |
| --------- | ------------------------------------------------- |
| URL | https://api.lisglo.com/api/openapi/v1/sms/uplinks |
| HTTP 方法 | GET |
| 请求体 | 无 |
| 是否鉴权 | 是,按第 2 节 GET 规则签名 |
| 所需能力 | 上行查询 |
| 成功状态 | 200 OK |
| 排序 | 接收时间、记录编号倒序 |
### 5.2 查询参数
| 参数 | 类型 | 必填 | 默认值 / 说明 |
| ------------ | ------ | ---- | ------------------------------------------------- |
| mobile | string | 否 | 1 开头的 11 位手机号,精确匹配 |
| accessNumber | string | 否 | 121 位数字接入号,精确匹配 destId |
| keyword | string | 否 | 筛选正文包含指定关键词的回复 |
| limit | 正整数 | 否 | 默认 50;按应用最大分页配置裁剪,系统默认上限 100 |
| cursor | string | 否 | 首次不传;下一页使用上一响应的 nextCursor |
不使用的筛选项省略,所有 query 参数都不放入签名原文。
### 5.3 请求示例
```bash
curl --include --max-time 15 --get \
"https://api.lisglo.com/api/openapi/v1/sms/uplinks" \
--data-urlencode "limit=25" \
--header "X-App-Key: $ACCESS_KEY" \
--header "X-Timestamp: $TIMESTAMP" \
--header "X-Nonce: $NONCE" \
--header "X-Signature: $SIGNATURE"
```
**HTTP 请求报文:**
```http
GET /api/openapi/v1/sms/uplinks?limit=25 HTTP/1.1
Host: api.lisglo.com
Accept: */*
X-App-Key: DEMO_ACCESS_KEY_NOT_REGISTERED
X-Timestamp: 1789355443
X-Nonce: 567a9bdd-7d62-4097-9fa8-50586fa14c32
X-Signature: 7ca4cb9fe5fb455d9314797d00eeab797257e2671d0a547414122e971f534214
```
### 5.4 返回结果
**查询成功:**
```http
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
}
```
| 字段 | 类型 | 说明 |
| ---------- | ----------- | --------------------------------- |
| items | array | 本页上行记录,没有记录时为空数组 |
| nextCursor | string/null | 下一页凭据;null 表示本次查询结束 |
items 中每条记录的字段:
| 字段 | 类型 | 说明 |
| ----------- | ----------- | ----------------------------------- |
| id | string | 上行记录编号,用于查询详情 |
| messageId | string/null | 关联的平台短信编号,未关联时为 null |
| phoneNumber | string | 回复短信的手机号 |
| destId | string | 接收回复的短信接入号 |
| content | string | 用户回复的正文 |
| receivedAt | string | 接收时间,ISO 8601Z 表示 UTC |
**没有回复:**
```json
{
"items": [],
"nextCursor": null
}
```
HTTP 状态为 200。无总条数或页码字段。
**分页参数错误:**
```http
HTTP/1.1 400 Bad Request
Content-Type: application/problem+json; charset=utf-8
{
"type": "https://cmpp-platform.local/problems/limit_invalid",
"title": "BAD_REQUEST",
"status": 400,
"code": "LIMIT_INVALID",
"detail": "limit必须为正整数"
}
```
### 5.5 下一页查询
nextCursor 非 null 时,将完整值赋给 $CURSOR,保持首次使用的 limit 和其他筛选不变,重新生成鉴权头。假设第一页使用 limit=25,下一页示例:
```bash
curl --include --max-time 15 --get \
"https://api.lisglo.com/api/openapi/v1/sms/uplinks" \
--data-urlencode "limit=25" \
--data-urlencode "cursor=$CURSOR" \
--header "X-App-Key: $ACCESS_KEY" \
--header "X-Timestamp: $TIMESTAMP" \
--header "X-Nonce: $NONCE" \
--header "X-Signature: $SIGNATURE"
```
**HTTP 请求报文:**
```http
GET /api/openapi/v1/sms/uplinks?limit=25&cursor=WyIyMDI2LTA5LTE0VDAyOjA1OjAwLjAwMFoiLCJ1cGxpbmstZXhhbXBsZS0wMDEiXQ HTTP/1.1
Host: api.lisglo.com
Accept: */*
X-App-Key: DEMO_ACCESS_KEY_NOT_REGISTERED
X-Timestamp: 1789355443
X-Nonce: 2232e6cf-144d-41c6-ac86-df7fb6184d78
X-Signature: 50869db4ff5a3b2d3ba5e6c1184020fc0ed50fbc594a56ccc6e2a16f7b617657
```
直到 nextCursor 为 null 后停止。游标由平台返回,不自行生成或解码;格式错误返回 400 CURSOR_INVALID。每次默认查询范围随当前时间滚动,同一轮翻页应尽快完成,定期采集按 id 去重。历史补查或完整历史同步请联系平台。
## 6. API:上行短信详情查询
### 6.1 接口说明
查看一条用户回复的完整业务信息。
| 项目 | 内容 |
| --------- | ------------------------------------------------------------ |
| URL | https://api.lisglo.com/api/openapi/v1/sms/uplinks/{uplinkId} |
| HTTP 方法 | GET |
| 请求体 | 无 |
| 是否鉴权 | 是,按第 2 节 GET 规则签名 |
| 所需能力 | 上行查询 |
| 成功状态 | 200 OK |
### 6.2 路径参数
| 参数 | 类型 | 必填 | 说明 |
| -------- | ------ | ---- | ------------------------------------------------------- |
| uplinkId | string | 是 | 列表中的 id 或回调中的 uplinkMessageId,按 URL 路径编码 |
### 6.3 请求示例
```bash
curl --include --max-time 15 --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 请求报文:**
```http
GET /api/openapi/v1/sms/uplinks/uplink-example-001 HTTP/1.1
Host: api.lisglo.com
Accept: */*
X-App-Key: DEMO_ACCESS_KEY_NOT_REGISTERED
X-Timestamp: 1789355443
X-Nonce: 7229fc46-7526-4726-a42b-a8edfe68fbdf
X-Signature: 869e57abbd91ceaadb73dbca1a091d0deba083e93acccc58cc4a941d9c18a1c8
```
### 6.4 返回结果
**查询成功:**
```http
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"
}
```
字段与第 5 节单条记录相同,增加 tenantId(本企业 ID)、applicationId(本应用 ID);只返回客户自己的业务信息,不提供通道字段。
**查询失败:**
```http
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": "上行记录不存在"
}
```
## 7. Webhook:短信回执通知
### 7.1 通知说明
短信产生回执结果时,平台向客户配置的回执地址发送 receipt 事件。
| 项目 | 内容 |
| ------------ | ------------------------------------- |
| URL | 客户在回调配置中保存的公网 HTTPS 地址 |
| HTTP 方法 | POST |
| Content-Type | application/json |
| 事件类型 | receipt |
| 签名密钥 | 回执回调地址对应的密钥 |
| 成功确认 | 客户返回任意 HTTP 2xx |
参数错误或未受理的同步请求不保证生成回调;长短信按业务消息通知。保存发送响应中的编号映射,按 data.messageId 关联业务,回调不保证携带 clientMessageId。
### 7.2 请求头
| 请求头 | 说明 |
| ------------ | --------------------------------------- |
| Content-Type | application/json |
| X-Event-Id | 事件编号,用于去重 |
| X-Event-Type | receipt(回执)或 uplink(用户回复) |
| X-Timestamp | 本次投递的 Unix 秒级时间戳 |
| X-Signature | sha256= 前缀加 HMAC-SHA256 十六进制签名 |
### 7.3 请求字段
| 字段 | 类型 | 说明 |
| ---------- | ------ | ---------------------------------- |
| eventId | string | 同一事件的唯一编号,重复投递时不变 |
| eventType | string | 事件类型 |
| occurredAt | string | 事件生成时间,ISO 8601 |
| data | object | 对应事件的业务数据 |
receipt 事件的 data 字段:
| data 字段 | 类型 | 说明 |
| ---------------- | ----------- | -------------------------------------------- |
| messageId | string | 平台短信编号,与发送响应关联 |
| gatewayMessageId | string/null | 短信传输编号,可缺省;关联业务使用 messageId |
| phoneNumber | string | 接收短信的手机号 |
| receiptStatus | string | 回执结果,delivered 表示送达 |
| rawStatus | string/null | 原始回执状态,如 DELIVRD;可缺省 |
| errorCode | string/null | 错误码,可缺省 |
| errorMessage | string/null | 错误说明,部分失败事件提供 |
| deliveredAt | string/null | 结果时间,可缺省;时间存在不等于发送成功 |
### 7.4 通知示例
```http
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"
}
}
```
地址和签名为说明用途,实际平台使用客户配置的地址并生成本次签名。
### 7.5 验证通知签名
回调验签与接口请求签名不同。回调只对“时间戳、一个 LF 换行、原始请求体”签名,使用该回调地址对应的签名密钥。
```text
签名原文 = UTF8(X-Timestamp) + LF + 原始请求体字节
期望签名 = "sha256=" + HEX(HMAC_SHA256(回调签名密钥, 签名原文))
```
接收端按以下顺序处理:
1. 保留原始请求体字节,不要解析后重新生成 JSON 再验签。
2. 检查时间戳为秒级时间戳并校验时间偏差;接收端可采用前后 300 秒的策略。
3. 计算期望签名,使用常量时间比较方法与 X-Signature 比较。
4. 验签通过后解析 JSON,核对请求头与正文中的事件编号、事件类型一致。
5. 以 eventId 做持久化唯一约束或原子去重,再保存事件。
同一事件重试时,eventId 和事件正文不变,投递时间戳和签名重新生成。已成功保存的重复事件正常确认,不重复执行后续业务。occurredAt 是事件生成时间,短信实际结果或回复时间分别取 data.deliveredAt、data.receivedAt。
### 7.6 接收确认与失败重试
平台将任意 HTTP 2xx 视为接收成功,不要求固定响应正文。建议验签并可靠保存后及时确认,后续业务异步处理。
**接收成功响应示例:**
```http
HTTP/1.1 200 OK
Content-Type: text/plain; charset=utf-8
OK
```
**暂时无法保存时的响应示例:**
```http
HTTP/1.1 503 Service Unavailable
Content-Type: text/plain; charset=utf-8
temporarily unavailable
```
| 接收端结果 | 平台处理 |
| ----------------------- | ---------------------------------------------------- |
| 任意 2xx | 视为成功,不再自动重试;即使正文写“失败”也按成功处理 |
| 网络错误、408、429、5xx | 在应用开启重试且未达到次数上限时安排重试 |
| 其他 4xx | 视为失败,通常不自动重试 |
| 3xx | 不跟随重定向 |
默认最多尝试 7 次,包含第一次投递。第一次失败后依次等待 1 分钟、5 分钟、15 分钟、1 小时、6 小时、24 小时;这些是相邻尝试之间的等待时间,具体受应用重试配置限制。默认回调超时配置为 10 秒,请尽快完成可靠保存和确认。
回调未收到不等于短信发送失败,可使用短信回执查询接口核对结果。回调可能重复或延迟到达,业务系统应按事件编号去重。
## 8. Webhook:上行短信通知
### 8.1 通知说明
收到并确认归属本应用的用户回复后,平台向上行回调地址发送 uplink 事件。
| 项目 | 内容 |
| ------------ | ----------------------------------------- |
| URL | 客户在回调配置中保存的上行公网 HTTPS 地址 |
| HTTP 方法 | POST |
| Content-Type | application/json |
| 事件类型 | uplink |
| 签名密钥 | 上行回调地址对应的密钥 |
| 成功确认 | 客户返回任意 HTTP 2xx |
### 8.2 请求字段
公共请求头和事件外层字段与第 7 节相同,事件类型为 uplink。data 包含:
| data 字段 | 类型 | 说明 |
| --------------- | ----------- | ---------------------------------------- |
| applicationId | string | 本应用 ID |
| phoneNumber | string | 回复者手机号 |
| destId | string | 接收回复的短信接入号 |
| content | string | 回复正文 |
| receivedAt | string | 接收时间,ISO 8601 |
| uplinkMessageId | string | 上行记录编号,可用于详情查询 |
| messageId | string/null | 可能提供的关联短信编号;缺省时不强行关联 |
| manualClaim | boolean | 可选;true 表示该回复经人工确认归属 |
### 8.3 通知示例
```http
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"
}
}
```
没有 messageId 时,按 uplinkMessageId 保存用户回复,不强行关联发送记录。
### 8.4 验签与确认
按第 7.5 节规则验签,但使用上行回调密钥。以 eventId 原子去重并可靠保存后,按第 7.6 节返回 2xx;失败重试规则相同。
```http
HTTP/1.1 200 OK
Content-Type: text/plain; charset=utf-8
OK
```
## 9. HTTP 状态与错误码
错误响应示例:
```json
{
"type": "https://cmpp-platform.local/problems/signature_invalid",
"title": "UNAUTHORIZED",
"status": 401,
"code": "SIGNATURE_INVALID",
"detail": "请求签名校验失败"
}
```
| HTTP | code | 处理方法 |
| ---- | ----------------------------------------------------------------------------- | ---------------------------------------------------------------- |
| 400 | `PARAMETER_INVALID` / `LIMIT_INVALID` | 检查字段类型、长度和正整数分页 |
| 409 | `REQUEST_REQUIRES_REVIEW` | 提供 requestId 核对,不更换幂等键重发 |
| 400 | `MOBILE_INVALID` / `CONTENT_REQUIRED` | 检查单个手机号和正文 |
| 400 | `IDEMPOTENCY_KEY_INVALID` | 补齐有效幂等键,检查长度与字符 |
| 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、签名原文、路径、原始请求体及 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` 等 | 保留请求标识和时间联系平台;发送场景先查询,避免重新创建业务 |
遇到未知状态或错误码时,保留原始响应并联系平台;不要默认成功。网络中断也可能没有 HTTP 响应。发送结果不确定时先核对已有记录,避免创建重复业务。
联系平台排查时,请提供应用名称、请求时间及时区、接口路径、HTTP 状态、业务 code,以及相关 requestId、messageId、clientMessageId 或 eventId。响应头 X-Request-Id 用于定位本次 HTTP 调用;发送正文 requestId 用于定位该次业务受理。不要提供 Secret 或完整鉴权信息。