Webhook 回调事件
Webhook 是一种基于 HTTP 协议的回调机制,允许服务端主动推送数据。声网的消息通知服务使用 Webhook 向你推送特定事件的通知,当你订阅的事件发生时,声网业务服务器会将事件消息发送给声网消息通知服务器,然后声网消息通知服务器会通过 HTTPS POST 请求将事件通知投递给你的服务器。

适用场景
互动白板的消息通知服务可以在业务层维护一个实时同步的状态机,实现实时监听文档转换任务的进度和运行状态信息。
前提条件
开始之前,请确保你满足以下条件:
- 已开启文档转换服务并配置第三方云存储的有效声网项目。详见使用文档转换服务。
- 满足以下条件的服务器:
- 支持 HTTPS 协议。为提高安全性,声网消息通知服务不再支持 HTTP 服务器地址。
- (推荐)支持 HTTPS 连接重用,即 keep-alive 模式,以降低消息投递延时。声网建议进行如下设置:
MaxKeepAliveRequests
:大于等于 100。KeepAliveTimeout
:大于等于 10 秒。
启用文档转换 Webhook 回调
调用发起文档转换时,在请求包体中的 webhookEndpoint
字段传入 Webhook 回调地址即可对本次转换启用文档转换 Webhook 回调,接收来自服务端的文档转换进度和运行状态信息,回调内容和字段解释详见 Webhook 回调内容。
curl --request POST \
--url https://api.netless.link/v5/projector/tasks \
--header 'Accept: application/json' \
--header 'Content-Type: application/json' \
--header 'region: cn-hz' \
--header 'token: NETLESSSDK_YWs9xxxxxxM2MjRi' \
--data '{
"resource": "https://docs-test-xx.oss-cn-hangzhou.aliyuncs.com/xxx",
"type": "dynamic",
"preview": true,
# 配置 Webhook 回调地址
"webhookEndpoint": "https://example.com/agoracallback",
# 配置 Webhook 回调失败时的重试次数
"webhookRetry": 3,
"imageCompressionLevel": 0
}'
转换任务完成时,声网消息服务器会以 HTTPS POST 请求的形式向你的服务器发送消息通知回调。数据格式为 JSON,字符编码为 UTF-8,签名算法为 HMAC/SHA1 或 HMAC/SHA256。
验证签名
为提高声网消息服务器和你的服务器之间的通信安全,你可以通过签名机制进行身份验证,确保接收到的请求来自声网。
当声网向你的服务器发送消息通知回调时,会使用密钥通过 HMAC/SHA1 和 HMAC/SHA256 算法生成签名值,并分别添加在 HTTPS 请求 Header 的 Agora-Signature
和 Agora-Signature-V2
字段中。
参考以下步骤进行签名验证:
-
获取密钥:登录控制台,在互动白板-功能配置页面点击生成 webhook 密钥为你的项目生成一个文档转换 Webhook 密钥。
-
收到回调后,使用密钥和请求包体里的参数,选用 HMAC/SHA1 或 HMAC/SHA256 算法计算签名值。
-
将你计算出的签名与请求 Header 中对应的字段进行对比:
- 如果你选用 HMAC/SHA1 算法:将计算值与
Agora-Signature
字段对比。二者完全相同则说明该请求是由声网发送的。 - 如果你选用 HMAC/SHA256 算法:将计算值与
Agora-Signature-V2
字段对比。二者完全相同则说明该请求是由声网发送的。
- 如果你选用 HMAC/SHA1 算法:将计算值与
声网提供多种语言的验证签名示例代码供你参考。
- HMAC/SHA256
- HMAC/SHA1
如下代码中的 request_body
是反序列化之前的 binary byte array,不是反序列化之后的 Dictionary。
#!/usr/bin/env python2
# !-*- coding: utf-8 -*-
import hashlib
import hmac
# 拿到消息通知的 raw request body 并对其计算签名
request_body = '{"type": "dynamic_conversion","taskId": "c705b8axxxxxxxxx669421","time": 1724307537510,"prefixUrl": "https://example.com/preview/dynamicConvert","status": {"code": 0"message": "ok"},"pageCount": 10,}'
secret = 'secret'
signature2 = hmac.new(secret, request_body, hashlib.sha256).hexdigest()
print(signature2)
如下代码中的 request_body
是反序列化之前的 binary byte array,不是反序列化之后的 Dictionary。
#!/usr/bin/env python2
# !-*- coding: utf-8 -*-
import hashlib
import hmac
# 拿到消息通知的 raw request body 并对其计算签名
request_body = '{"type": "dynamic_conversion","taskId": "c705b8axxxxxxxxx669421","time": 1724307537510,"prefixUrl": "https://example.com/preview/dynamicConvert","status": {"code": 0"message": "ok"},"pageCount": 10,}'
secret = 'secret'
signature = hmac.new(secret, request_body, hashlib.sha1).hexdigest()
print(signature)
- HMAC/SHA256
- HMAC/SHA1
如下代码中的 requestBody
是反序列化之前的 binary byte array,不是反序列化之后的 Object。
const crypto = require('crypto')
// 拿到消息通知的 raw request body 并对其计算签名
const requestBody = '{"type": "dynamic_conversion","taskId": "c705b8axxxxxxxxx669421","time": 1724307537510,"prefixUrl": "https://example.com/preview/dynamicConvert","status": {"code": 0"message": "ok"},"pageCount": 10,}'
const secret = 'secret'
const signature2 = crypto.createHmac('sha256', secret).update(requestBody, 'utf8').digest('hex')
console.log(signature2)
如下代码中的 requestBody
是反序列化之前的 binary byte array,不是反序列化之后的 Object。
const crypto = require('crypto')
// 拿到消息通知的 raw request body 并对其计算签名
const requestBody = '{"type": "dynamic_conversion","taskId": "c705b8axxxxxxxxx669421","time": 1724307537510,"prefixUrl": "https://example.com/preview/dynamicConvert","status": {"code": 0"message": "ok"},"pageCount": 10,}'
const secret = 'secret'
const signature = crypto.createHmac('sha1', secret).update(requestBody, 'utf8').digest('hex')
console.log(signature)
- HMAC/SHA256
- HMAC/SHA1
如下代码中的 request_body
是反序列化之前的 binary byte array,不是反序列化之后的 Object。
如下代码中的 request_body
是反序列化之前的 binary byte array,不是反序列化之后的 Object。
- HMAC/SHA256
- HMAC/SHA1
- HMAC/SHA256
- HMAC/SHA1
如下代码中的 request_body
是反序列化之前的 binary byte array,不是反序列化之后的 Dictionary。
package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"fmt"
)
func main() {
// 拿到消息通知的 raw request body 并对其计算签名
request_body := `{"type": "dynamic_conversion","taskId": "c705b8axxxxxxxxx669421","time": 1724307537510,"prefixUrl": "https://example.com/preview/dynamicConvert","status": {"code": 0"message": "ok"},"pageCount": 10,}`
secret := "secret"
fmt.Println(calcSignatureV2(secret, request_body))
}
func calcSignatureV2(secret, payload string) string {
mac := hmac.New(sha256.New, []byte(secret))
mac.Write([]byte(payload))
return hex.EncodeToString(mac.Sum(nil))
}
如下代码中的 request_body
是反序列化之前的 binary byte array,不是反序列化之后的 Dictionary。
package main
import (
"crypto/hmac"
"crypto/sha1"
"encoding/hex"
"fmt"
)
func main() {
// 拿到消息通知的 raw request body 并对其计算签名
request_body := `{"type": "dynamic_conversion","taskId": "c705b8axxxxxxxxx669421","time": 1724307537510,"prefixUrl": "https://example.com/preview/dynamicConvert","status": {"code": 0"message": "ok"},"pageCount": 10,}`
secret := "secret"
fmt.Println(calcSignatureV1(secret, request_body))
}
func calcSignatureV1(secret, payload string) string {
mac := hmac.New(sha1.New, []byte(secret))
mac.Write([]byte(payload))
return hex.EncodeToString(mac.Sum(nil))
}
参考信息
开发注意事项
- 声网消息通知服务不保证消息通知完全按照事件发生的顺序到达,你的服务器需要能处理乱序消息。
- 为提高消息服务的可靠性,每次事件可能会有不止一次消息通知,你的服务器需要能处理重复消息。
Webhook 回调内容
请求 Header
消息通知回调的请求头部包含以下字段:
字段名 | 值 |
---|---|
Content-Type | Application/json |
Agora-Signature | 声网用客户密钥和 HMAC/SHA1 算法生成的签名值。你需要使用客户密钥和 HMAC/SHA1 算法来验证该签名值。详见验证签名。 |
Agora-Signature-V2 | 声网用客户密钥和 HMAC/SHA256 算法生成的签名值。你需要使用客户密钥和 HMAC/SHA256 算法来验证该签名值。详见验证签名。 |
请求 Body
消息通知回调的请求包体包含以下字段:
字段 | 类型 | 描述 |
---|---|---|
code | Number | 转换任务状态码,转换成功时为 0 ,失败时会返回错误码,详见文档转换错误码。 |
message | String | 错误码对应的错误消息,描述出错的原因。仅在转换任务失败时,才会返回该字段。 |
data | JSON Object | 转换任务相关数据,包含以下字段:taskId :String。转换任务的 UUID,即转换任务的唯一标识符。taskType :转换任务类型,取值如下:
prefixUrl :String。转换结果文件地址前缀路径。pageCount :Number。文档页数,转换任务失败时没有该字段。previews :JSON Object。转换后的文档预览图地址,每页对应一个预览图地址。该参数仅在发起文档转换时,请求包体中的 preview 设为 true ,且 type 设为 dynamic 时才生效。images :JSON Object。文档转图片结果的地址和图片参数,每页对应一张图片。
type 设为 static 时才生效。note :String。从文档中提取出的所有备注内容。noticeTimestamp :Number。发送请求的时间戳。 |
以下为请求包体示例:
{
"code": 0,
"message": "ok",
"data": {
"taskId": "0d2c7604b730xxxxxxxxx1e31d344c",
"taskType": "dynamic_convert",
"pageCount": 5,
"previews": {
"1": "https://conversion-demo-cn.oss-cn-hangzhou.aliyuncs.com/demo/dynamicConvert/0d2c7604b730xxxxxxxxx1e31d344c/preview/1.png",
"2": "https://conversion-demo-cn.oss-cn-hangzhou.aliyuncs.com/demo/dynamicConvert/0d2c7604b730xxxxxxxxx1e31d344c/preview/2.png",
"3": "https://conversion-demo-cn.oss-cn-hangzhou.aliyuncs.com/demo/dynamicConvert/0d2c7604b730xxxxxxxxx1e31d344c/preview/3.png",
"4": "https://conversion-demo-cn.oss-cn-hangzhou.aliyuncs.com/demo/dynamicConvert/0d2c7604b730xxxxxxxxx1e31d344c/preview/4.png",
"5": "https://conversion-demo-cn.oss-cn-hangzhou.aliyuncs.com/demo/dynamicConvert/0d2c7604b730xxxxxxxxx1e31d344c/preview/5.png"
},
"note": "https://conversion-demo-cn.oss-cn-hangzhou.aliyuncs.com/demo/dynamicConvert/0d2c7604b730xxxxxxxxx1e31d344c/jsonOutput/note.json",
"prefix": "https://conversion-demo-cn.oss-cn-hangzhou.aliyuncs.com/demo/dynamicConvert",
"noticeTimestamp": 1724322571541
}
}