HTTP API
HTTP API 使用统一域名 https://api-tistore.tange365.com,分为两组:
/openapi/v1面向可信业务服务端,使用 TGV1 签名;/sdk/v1面向持有短期 APP Access Token 的客户端,使用 Bearer Token。
业务服务端当前公开 4 个接口:
| 方法 | 路径 | 用途 |
|---|---|---|
| POST | /issue-device-access-token | 为设备签发上传所需的 device_access_token |
| POST | /issue-app-access-token | 为客户端签发访问目标设备录像的 app_access_token |
| GET | /query-recording-file-indexes | 分页查询录像文件索引 |
| POST | /delete-recordings | 删除指定设备和时间范围内的录像 |
签发 Token 前,先按 云端签发 Token 完成凭证保管和请求签名。查询或删除录像时,参阅 管理云录像。
客户端可以调用 GET /sdk/v1/query-recording-days 查询指定范围内哪些日期有录像。通常应优先使用客户端 SDK 的 listRecordingDays 包装;直接接入 HTTP 时,按查询有录像的日期发送请求。
业务服务端 API 鉴权
/openapi/v1 请求使用 TGV1-HMAC-SHA256。AppId、AccessKeyId 和 AccessKeySecret 只保存在可信业务服务端。请求时间与服务端相差在 300 秒以内。
请求 Header
| Header | 说明 |
|---|---|
X-Tg-Algorithm | 固定为 TGV1-HMAC-SHA256 |
X-Tg-App-Id | 当前云存储应用的 AppId |
X-Tg-Date | UTC 时间,格式为 yyyyMMdd'T'HHmmss'Z' |
X-Tg-Content-Sha256 | 最终发送 Body 原始字节的 SHA-256 小写十六进制值;无 Body 时使用空字节摘要 |
X-Tg-Signed-Headers | 参与签名的 Header 名称,以小写、严格升序、分号连接 |
Authorization | AccessKeyId、Credential Scope、SignedHeaders 和最终签名 |
POST 请求发送 Content-Type: application/json 和实际 Content-Length,并把这两个 Header 加入 SignedHeaders。SignedHeaders 不含 Host。
1. 构造 Canonical Request
Canonical Request 由以下六行组成,行间使用 LF,末尾不追加换行:
HTTP_METHOD
CANONICAL_URI
CANONICAL_QUERY
CANONICAL_HEADERS
SIGNED_HEADER_NAMES
BODY_SHA256HTTP_METHOD:使用大写方法名。CANONICAL_URI:对 URL 的 escaped path 再执行 RFC 3986 Path 转义;普通 HTTP API 路径保持/openapi/v1/...。CANONICAL_QUERY:按参数名排序;同名参数的值也排序;名称和值使用 RFC 3986 编码,空格编码为%20,同名参数全部保留。CANONICAL_HEADERS:只包含 SignedHeaders,Header 名转小写并严格升序,值去除首尾空白,每项写作name:value并以 LF 分隔。SIGNED_HEADER_NAMES:与X-Tg-Signed-Headers完全一致。BODY_SHA256:最终发送 Body 原始字节的 SHA-256 小写十六进制值。
必签 Header 包括 X-Tg-Algorithm、X-Tg-App-Id 和 X-Tg-Date。POST、PUT、PATCH 还要签入 Content-Type 和实际 Content-Length。X-Tg-Content-Sha256 与 Body Hash 一致,SignedHeaders 不含该字段。
额外发送的 X-Tg-* Header 也要加入 SignedHeaders,但以下传输字段除外:
X-Tg-Signed-HeadersX-Tg-Content-Sha256- Credential
- Signature
每个被签 Header 只发一个值。
2. 构造 String to Sign
Credential Scope 使用“签名 UTC 日期加 7 天”的日期,格式为:
YYYYMMDD/tgv1_request这里的“加 7 天”是 TGV1 协议约定,不表示请求或 Token 有效 7 天。String to Sign 由四行组成:
TGV1-HMAC-SHA256
X-Tg-Date
CredentialScope
HexLower(SHA256(CanonicalRequest))3. 派生签名
按以下顺序计算 HMAC-SHA256;前三步保留原始摘要字节,最后一步才转为小写十六进制:
kDate = HMAC-SHA256(key="TGV1" + AccessKeySecret, message=UTC_SIGNING_DATE)
kPath = HMAC-SHA256(key=kDate, message=URL_PATH)
kSigning = HMAC-SHA256(key=kPath, message="tgv1_request")
Signature = HexLower(HMAC-SHA256(key=kSigning, message=StringToSign))UTC_SIGNING_DATE 取签名当天的 YYYYMMDD。Credential Scope 使用加 7 天后的日期。URL_PATH 使用解码后的 URL Path,和 Canonical URI 的二次百分号编码分开计算。
4. 设置 Authorization
TGV1-HMAC-SHA256 Credential=<AccessKeyId>/<CredentialScope>, SignedHeaders=<SignedHeaders>, Signature=<Signature>Authorization 中的 SignedHeaders 必须与 X-Tg-Signed-Headers 完全一致。
固定测试向量
先使用下面的非生产凭证验证签名器。只有输出与预期 Authorization 完全一致,才能替换真实凭证和业务参数。
Method: POST
Path: /openapi/v1/issue-device-access-token
Raw Query: z=last&a=two&a=one
Body: {"device_id":"device-001"}
Signing Time: 2026-08-10T01:02:03Z
AppId: app-001
AccessKeyId: AK_TEST_001
AccessKeySecret: test-secret-not-real请求 Header:
Content-Length: 26
Content-Type: application/json
X-Tg-Algorithm: TGV1-HMAC-SHA256
X-Tg-App-Id: app-001
X-Tg-Content-Sha256: f1e6aa2231dd0d25e64a2cf4ae2348a1a165ad5a442e6429ab29e62bf87cc16f
X-Tg-Date: 20260810T010203Z规范化后的 Query、Scope 和 SignedHeaders:
Canonical Query: a=one&a=two&z=last
Credential Scope: 20260817/tgv1_request
SignedHeaders: content-length;content-type;x-tg-algorithm;x-tg-app-id;x-tg-date预期 Authorization:
TGV1-HMAC-SHA256 Credential=AK_TEST_001/20260817/tgv1_request, SignedHeaders=content-length;content-type;x-tg-algorithm;x-tg-app-id;x-tg-date, Signature=2fd626b889af8cc4bd87fc9aaaf84f5fe8eb49438387308c7c6a42ce73b50c5b生产日志记录接口名、状态码、code 和 request_id。
请求与响应 JSON
成功和失败都使用统一 JSON 包络,业务字段在 data:
{
"code": "OK",
"message": "成功",
"request_id": "req_xxx",
"data": {}
}| 字段 | 类型 | 说明 |
|---|---|---|
code | string | 程序判断依据。成功为 OK |
message | string | 给人看的说明 |
request_id | string | 单次请求的排查线索 |
data | object | 业务字段。失败时为空对象 {} |
整数按位置这样写:
| 位置 | 写法 | 例子 |
|---|---|---|
| JSON Body | JSON number | "duration_seconds": 3600、"start_time_ms": 1786896000000 |
| Query | 十进制数字 | start_time_ms=1786896000000 |
响应 data 中的时间戳和字节数 | 十进制字符串 | "expires_at_ms": "1786899600000" |
响应里按字符串接收 expires_at_ms、start_time_ms、end_time_ms、size_bytes、total_size_bytes。duration_seconds 和 recording_retention_days 是 JSON number。has_more 和 effective 是 boolean。
POST 的 Body 是 JSON 对象,字段以下表为准,Content-Type 为 application/json。GET 把参数放在 Query 里。device_id 填写要操作的目标设备的标识。
签发 device_access_token
POST /openapi/v1/issue-device-access-token
Content-Type: application/json
{
"device_id": "device-001",
"duration_seconds": 3600,
"recording_retention_days": 7
}| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
device_id | string | 是 | 要签发上传 Token 的设备的标识 |
duration_seconds | number | 是 | Token 有效期,900~43200 秒 |
recording_retention_days | number | 是 | 写入设备 Token 的录像保存天数,正整数 |
成功响应:
{
"code": "OK",
"message": "成功",
"request_id": "req_xxx",
"data": {
"access_token": "opaque-token",
"expires_at_ms": "1786899600000",
"recording_retention_days": 7
}
}| 字段 | 类型 | 说明 |
|---|---|---|
access_token | string | 下发给设备的 device_access_token |
expires_at_ms | string | Token 到期时间,UTC Unix 毫秒 |
recording_retention_days | number | 实际写入 Token 的保存天数 |
设备把该 Token 传给 C SDK 后上传录像。
签发 app_access_token
POST /openapi/v1/issue-app-access-token
Content-Type: application/json
{
"device_id": "device-001",
"duration_seconds": 3600
}| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
device_id | string | 是 | 要签发 Token 的目标设备标识,客户端将访问这台设备的录像 |
duration_seconds | number | 是 | Token 有效期,900~43200 秒 |
成功响应:
{
"code": "OK",
"message": "成功",
"request_id": "req_xxx",
"data": {
"access_token": "opaque-token",
"expires_at_ms": "1786899600000"
}
}| 字段 | 类型 | 说明 |
|---|---|---|
access_token | string | 下发给客户端的 app_access_token |
expires_at_ms | string | Token 到期时间,UTC Unix 毫秒 |
客户端用这个 Token 访问签发时指定的那台设备的录像。设备上传使用 device_access_token。
查询录像文件索引
GET /openapi/v1/query-recording-file-indexes?device_id=device-001&start_time_ms=1786896000000&end_time_ms=1786899600000&page_size=100查询范围是 UTC Unix 毫秒左闭右开区间 [start_time_ms, end_time_ms)。参数放在 Query 里。继续分页时,设备和时间范围与第一页相同,并把上一页的 next_page_token 填入 page_token。
| Query 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
device_id | string | 是 | 要查询录像索引的设备的标识 |
start_time_ms | string | 是 | UTC Unix 毫秒开始时间,包含 |
end_time_ms | string | 是 | UTC Unix 毫秒结束时间,不含该时刻。单次范围最长 31 天 |
page_size | string | 否 | 每页条数。省略或 0 时为 50,取值 1~200 |
page_token | string | 否 | 上一页返回的 next_page_token。第一页省略 |
成功响应:
{
"code": "OK",
"message": "成功",
"request_id": "req_xxx",
"data": {
"items": [
{
"oss_id": "oss-001",
"recording_retention_days": 7,
"start_time_ms": "1786896000000",
"end_time_ms": "1786899600000",
"total_size_bytes": "1048576",
"files": [
{
"path": "7/device-001/20260816/16-00-00-3600.data",
"size_bytes": "1048576",
"sha256": ""
}
]
}
],
"has_more": false,
"next_page_token": ""
}
}| 字段 | 类型 | 说明 |
|---|---|---|
items | object[] | 当前页的录像文件索引;没有命中时为空数组 |
has_more | boolean | 为 true 时继续请求下一页 |
next_page_token | string | 有下一页时,把它原样填入下一页的 page_token;has_more 为 false 时为空字符串,到此结束分页 |
items 中每个元素:
| 字段 | 类型 | 说明 |
|---|---|---|
oss_id | string | 录像所在的存储标识 |
recording_retention_days | number | 该录像的保存天数 |
start_time_ms | string | 物理文件开始时间,UTC Unix 毫秒,左闭 |
end_time_ms | string | 物理文件结束时间,UTC Unix 毫秒,右开 |
total_size_bytes | string | 文件总字节数 |
files | object[] | 该索引下的物理文件 |
files 中每个元素:
| 字段 | 类型 | 说明 |
|---|---|---|
path | string | 对象存储中的文件路径 |
size_bytes | string | 文件字节数 |
sha256 | string | 设备上报时可能附带的摘要。用 path 和 size_bytes 识别文件 |
每一页是发出该次请求时的查询结果。分页过程中如果有新的录像索引进入同一时间范围,后续页面可能与已读页面重复或漏项。要得到一份稳定清单,等该时间范围不再写入新索引后,再从第一页查起。
删除录像
POST /openapi/v1/delete-recordings
Content-Type: application/json
{
"device_id": "device-001",
"start_time_ms": 1786896000000,
"end_time_ms": 1786899600000
}| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
device_id | string | 是 | 要删除其录像的设备的标识 |
start_time_ms | number | 是 | UTC Unix 毫秒开始时间,包含 |
end_time_ms | number | 是 | UTC Unix 毫秒结束时间,不含该时刻 |
删除范围是 UTC Unix 毫秒左闭右开区间 [start_time_ms, end_time_ms)。响应里的起止时间与这次请求提交的范围一致。
成功响应:
{
"code": "OK",
"message": "成功",
"request_id": "req_xxx",
"data": {
"start_time_ms": "1786896000000",
"end_time_ms": "1786899600000",
"effective": true
}
}| 字段 | 类型 | 说明 |
|---|---|---|
start_time_ms | string | 这次请求提交的开始时间,UTC Unix 毫秒 |
end_time_ms | string | 这次请求提交的结束时间,UTC Unix 毫秒 |
effective | boolean | 这组精确参数是否第一次写入删除记录 |
相同 AppId、device_id、start_time_ms 和 end_time_ms 再次请求时,第一次返回 true,之后返回 false。两个范围只要起止时间不同,就会各写一条记录。
客户端查询和播放会扣除已删除区间。用客户端查询确认删除是否生效。HTTP API 的文件索引按物理文件完整覆盖才隐藏该项;部分覆盖时仍返回完整文件。存储空间随后回收。
查询有录像的日期
客户端使用短期 app_access_token 调用此接口。Token 已经绑定目标设备,请求中不再传 device_id。
客户端 API 鉴权
调用时,把 APP Access Token 写入 Authorization Header。请求还要携带客户端平台和 SDK 版本。
GET /sdk/v1/query-recording-days?start_date=2026-08-01&end_date=2026-08-31&timezone=Asia%2FShanghai
Authorization: Bearer <app_access_token>
X-Tg-Platform: ANDROID
X-Tg-Sdk-Version: <sdk_version>每个请求必须且只能发送一个 Authorization、X-Tg-Platform 和 X-Tg-Sdk-Version Header。X-Tg-Platform 与 X-Tg-Sdk-Version 填写当前客户端平台和 SDK 版本。两者必须是有效的 UTF-8 字符串,长度为 1~64 字节,不能带首尾空白或控制字符。
APP 请求不要发送设备端专用的 X-Tg-Client-Id、X-Tg-Date、X-Tg-Signature 或 X-Tg-Nonce Header。即使这些 Header 的值为空,请求也会被拒绝。
| Query 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
start_date | string | 是 | 开始日期,严格使用 YYYY-MM-DD,包含当天 |
end_date | string | 是 | 结束日期,严格使用 YYYY-MM-DD,包含当天 |
timezone | string | 是 | 解释自然日边界的 IANA 时区 ID,例如 Asia/Shanghai 或 America/New_York |
起止日期均包含,单次最多查询 31 个自然日。时区必须使用有效的 IANA ID;不要传 UTC+8、Local 或带首尾空白的值。服务端按该时区的自然日边界查询,因此夏令时切换日不固定为 24 小时。
成功响应会按日期升序返回范围内的每一天,包括没有录像的日期:
{
"code": "OK",
"message": "成功",
"request_id": "req_xxx",
"data": {
"items": [
{
"date": "2026-08-01",
"has_recording": true
},
{
"date": "2026-08-02",
"has_recording": false
}
]
}
}| 字段 | 类型 | 说明 |
|---|---|---|
items | object[] | 查询范围内的完整逐日结果,按 date 升序排列 |
items[].date | string | YYYY-MM-DD 日期 |
items[].has_recording | boolean | 该自然日至少有一个当前可见的录像时点时为 true |
has_recording 只用于判断当天是否需要继续查询录像时间段。要取得可回放范围,请调用客户端 SDK 的 listRecordings。
| HTTP 状态 | code | 常见原因 |
|---|---|---|
| 400 | InvalidParameter | 日期或时区格式不合法,或客户端公共 Header 不合法 |
| 400 | InvalidParameter.TimeRange | 起止日期倒置,或范围超过 31 天 |
| 401 | AuthFailure.TokenMissing | 缺少 APP Access Token |
| 401 | AuthFailure.TokenInvalid | Token 无效或类型不匹配 |
| 401 | AuthFailure.TokenExpired | Token 已过期 |
| 403 | AccessDenied.Device | 当前应用无权访问 Token 绑定的设备 |
| 503 | ServiceUnavailable | 录像查询依赖暂时不可用 |
响应与诊断
用 HTTP 状态和 code 判断结果。排查时记录接口名、发生时间、状态码、code 和 request_id。
/openapi/v1 鉴权失败时,检查请求时间、AppId、签名输入和密钥配置。/sdk/v1/query-recording-days 鉴权失败时,检查 APP Access Token,以及 Authorization、X-Tg-Platform 和 X-Tg-Sdk-Version 是否各发送一次且格式合法。