回调通知
功能介绍
2023年5月15日之后注册的企业,将默认使用新回调通知,请参考新回调通知
若客户主动选择新回调,请与客户经理联系处理
通过回调通知,您可以在合同状态产生变化以及发生印章、模板、员工相关操作等场景接受到来自电子签的回调通知。
如您需要使用此功能,需提供能接受并处理 HTTP POST 请求的回调地址,请确保该地址提供的服务可公网访问并能正常处理回调通知。
电子签推送的回调内容有进行加密处理,在接受到回调通知后需用配置好的 Key 进行解密。
配置回调地址
登录 腾讯电子签控制台 在集成管理中,选择生产运行(如果当前为测试环境,请选择测试联调)。
在参数配置中设置回调地址,请确保填入的地址能够接受并处理 HTTP POST 请求,并返回状态码200表示处理正常。选择点击系统生成可以随机生成解密的Key。此处支持配置多个回调地址并同时生效。
支持的回调场景
- 合同发起以及签署相关回调
- 合同发起回调
- 签署人签署回调
- 签署流程签署完成回调
- 签署流程过期回调
- 签署流程撤销回调
- 签署流程拒签回调
- 签署流程填写回调
- 印章相关回调
- 创建印章回调
- 删除印章回调
- 停用印章回调
- 启用印章回调
- 印章授权回调
- 解除印章授权回调
- 印章审核通过回调
- 印章审核驳回回调
- 模板相关回调
- 创建模板回调
- 编辑模板回调
- 员工相关回调
- 员工认证成功回调
- 员工变更角色回调
- 扣费相关回调
- 合同扣费回调
!各类型回调的数据结构并不完全相同,请进行区分处理。
回调解密步骤
- 对收到的数据进行 Base64 解码得到密文。
- 对密文进行对称解密,算法为 AES-256-CBC,密钥为腾讯电子签提供的 CallbackUrlKey,IV 取 CallbackUrlKey 值的前16位,数据采用 PKCS#7 填充。
- 解密得到的数据为输入参数的 Json 格式。
合同发起以及签署回调通知参数
回调数据对象 FlowInfo 结构
参数名称 | 参数类型 | 参数描述 |
---|---|---|
FlowId | string | 流程编号。 |
DocumentId | string | 使用的文档 ID。 |
CallbackType | string | 回调的类型: sign:签署回调 review:审核回调 fill:填写回调 |
FlowName | string | 流程名称。 |
FlowType | string | 流程的类型。 |
FlowDescription | string | 流程的描述。 |
Unordered | bool | 流程类型顺序: true:为无序 false:为有序 |
CreateOn | int | 流程的创建时间戳。 |
UpdatedOn | int | 流程的修改时间戳。 |
DeadLine | int | 流程的过期时间0为永远不过期。 |
FlowCallbackStatus | int | 流程现在的状态: 1:待签署 2:部分签署 3:已拒签 4:已签署 5:已过期 6:已撤销 8:等待填写 21:已解除 |
UserId | string | 本环节需要操作人 UserId。 |
RecipientId | string | 签署区 ID。 |
Operate | string | 动作: start:发起 sign:签署 reject:拒签 cancel:取消 finish:结束 deadline:过期 fill:填写 |
UserData | string | 创建的时候设置的透传字段。 |
Approvers | Approver 数组 | 流程签约方列表。 |
FlowInfo 参数 Approver 结构
参数名称 | 参数类型 | 参数描述 |
---|---|---|
UserId | string | 本环节需要操作人的 UserId。 |
RecipientId | string | 签署区 ID。 |
ApproverType | int | 参与者类型: 0:企业 1:个人 3:企业静默签署 |
OrganizationName | string | 企业名称。 |
Required | bool | 是否需要签名。 |
ApproverName | string | 本环节需要操作人的名字。 |
ApproverMobile | string | 本环节需要操作人的手机号。 |
ApproverIdCardType | string | 签署人证件类型: ID_CARD:身份证。 HONGKONG_AND_MACAO:港澳居民来往内地通行证。 HONGKONG_MACAO_AND_TAIWAN:港澳台居民居住证(格式同居民身份证)。 |
ApproverIdCardNumber | string | 签署人证件号码。 |
ApproveCallbackStatus | int | 签署状态: 2:待签署 3:已签署 4:已拒绝 5:已过期 6:已撤销 8:待填写 9:因为各种原因而终止 10:填写 15:已解除 19:已转他人处理 |
ApproveMessage | string | 拒签的原因。 |
VerifyChannel | string | 签署意愿方式,WEIXINAPP:人脸识别。 |
ApproveTime | int | 签约的时间。 |
印章回调通知参数
回调数据对象 CallbackContent 结构
参数名称 | 参数类型 | 参数描述 |
---|---|---|
MsgId | string | 消息编号。 |
MsgType | string | 消息类型,固定为 OperateSeal。 |
MsgData | MsgData | 消息数据。 |
CallbackContent 参数 MsgData 结构
印章共有创建、删除、停用、启用、印章授权、解除印章授权、印章审核通过、印章审核驳回共8种回调。为方便描述区分,此处将印章审核通过、印章审核驳回归为“审核结果回调”类型,将其他回调归为“非审核结果回调”类型。
参数名称 | 参数类型 | 参数描述 |
---|---|---|
OrganizationId | string | 企业 ID。仅在非审核结果回调时有值。 |
OperatorUserId | string | 操作人 UserId。仅在非审核结果回调时有值。 |
SealId | string | 印章 ID。 |
SealName | string | 印章名称。 |
Operate | string | 动作,仅在非审核结果回调时有值: Create:创建 Delete:删除 Disable:停用 Enable:启用 Valid:印章授权 Invalid:解除印章授权 |
AuthorizedUsers | AuthorizedUser 数组 | 被授权用户数组,仅在 Operate 为 Enable,Disable 时有值。 |
ReviewStatus | string | 审核状态,仅在审核结果回调时有值: PASS:通过 REJECT:拒绝 |
ReviewReason | string | 审核通过或失败原因,仅在审核结果回调时有值。 |
MsgData 参数 AuthorizedUser 结构
参数名称 | 参数类型 | 参数描述 |
---|---|---|
Id | string | 被授权用户 ID。 |
Name | string | 被授权用户姓名。 |
模板回调通知参数
回调数据对象 CallbackContent 结构
参数名称 | 参数类型 | 参数描述 |
---|---|---|
MsgId | string | 消息编号。 |
MsgType | string | 消息类型: TemplateAdd:创建模板 TemplateUpdate:编辑模板 |
MsgData | MsgData | 消息数据。 |
CallbackContent 参数 MsgData 结构
参数名称 | 参数类型 | 参数描述 |
---|---|---|
OrganizationId | string | 企业 ID。 |
OperatorUserId | string | 操作人 UserId。 |
TemplateId | string | 模板 ID。 |
TemplateName | string | 模板名称。 |
CreateTime | int | 创建时间,秒级时间戳,仅在创建模板时有值。 |
UpdateTime | int | 更新时间,秒级时间戳,仅在编辑模板时有值。 |
员工回调通知参数
回调数据对象 CallbackContent 结构
参数名称 | 参数类型 | 参数描述 |
---|---|---|
MsgId | string | 消息编号。 |
MsgType | string | 消息类型: VerifyStaffInfo:员工认证成功 RolesChange:员工变更角色 |
MsgData | MsgData | 消息数据。 |
CallbackContent 参数 MsgData 结构
参数名称 | 参数类型 | 参数描述 |
---|---|---|
OrganizationId | string | 企业 ID。 |
UserId | string | 员工 UserId。 |
Name | string | 员工姓名。 |
BeforeRoleNames | string 数组 | 变更前角色名数组,仅在员工变更角色时有值。 |
AfterRoleNames | string 数组 | 变更后角色名数组,仅在员工变更角色时有值。 |
扣费回调通知参数
回调数据对象 CallbackContent 结构
参数名称 | 参数类型 | 参数描述 |
---|---|---|
MsgId | string | 消息编号。 |
MsgType | string | 消息类型: FlowCost:合同扣费。 |
MsgData | msgdata | 消息数据。 |
CallbackContent 参数 MsgData 结构
参数名称 | 参数类型 | 参数描述 |
---|---|---|
Cost | int | 扣除份额,如果为负值则为份额退还。 |
CostChannel | string | 扣费渠道。 |
FlowId | string | 合同 ID。 |
OrganizationId | string | 企业 ID。 |
IsResell | bool | 分销订单标识。 |
回调 FAQ
回调地址是否支持同时配置多个?
支持,回调地址可以同时存在多个,根据您的需求不同的地址可以配置相同或者不同的 CallbackUrlKey。
回调地址是否支持更改或删除?
支持,您可以在控制台集成管理中进行回调地址的配置。
回调地址配置后多长时间生效呢?
配置完成后立即生效。
为什么客户收到 FlowCallbackStatus 为4(已签署)的回调通知后,又收到了 FlowCallbackStatus 为1(待签署)的通知?
以单方签署的合同为例,FlowCallbackStatus 状态变化一般是由1变为4。少量回调可能因状态变化间隔比较短、重发、或者网络传输等原因,小几率出现到达顺序不一致,建议开发者从代码层面进行适当控制,例如状态更新为4后不能再更新为1。
电子签发送回调时超时时间是多久?
超时时间为5秒。
电子签发送回调失败后,回调最大重试次数是多少?重试机制是怎么样的呢?
回调的最大重试次数是5次;回调重试间隔随次数倍数增加,分别为500ms、1s、2s、4s、8s。
回调样例
回调请求包:
POST /callback HTTP/1.1
Host: www.esstest.com
User-Agent: Go-http-client/1.1
Content-Length: 1088
Content-Type: text/plain
Accept-Encoding: gzip
YyYyLZonMceFMFFi5jRnnOWrOasvzmKtGAvRPq1IzuYma88UvTqyZy8QpNVMKxvJY3Sp+NJW6mgTfU35u7SbUon+QCjul1P9P6mcVRuVvYrM2DoFBDgjLURfX+CWnZ9m967nNqiubw9vj9ToysJDZyr0zo4NN1CCfvsyxnVNKhSNbRAy74x4SlLscZ/wcFwdy55S2rBxbjLCqViIj6llQFo74mLHJ8oumngBD1WJZ5ginDNEScPB7+cIHeKF5w3UvUpDqDIUjAj7KFUmIQM8/zY8EafhgCNhWRaGxuFxGF+iMqwC+HJYosbBmrKZ44+8xwL5WlXLx/Cf8bK7J4mJIWbKyul8PBE9Xh8lL/d0Ufnf4sUB0ypbdy/KIr+XQJgFjR2AQGENXvxxlCfdVY5svGfXYaaSSyDND1u9C8kMxQRfNHJye7ulTprROYTtq4GJ8UJQbJbuHvTcppGyMbGO2AvgXcoSogM0JuZzLK/gcPFIWIf9oFTg47M62sLf9YY7UASVITfA5LnE+/1clN4vn748wjS4tdxCL8wjWanPOONTPCMrwH0wsZ86xEf7aLl0/qBWGF13VYh4C4XgiDLtaOs6DdlzMz5EszWISpRRzfJLxcBnhHL9sQu7YWLZzRL6vmP1qtdWZbUYt4Z/eKff5gfmmDGHOxVjd3XhxhfHSdW3a8LzlMT3n69CPBEiOjXA4abshkiT6+hOlJ8uCws+ja2BSmwruqpUn4tq7Je91cT0AhGHuvq9s1VCB7vw8KsVimRHrC6eOa1rgm6qgQNP0fMgGRe+qu4BtfND1a/j9BBuIHQSjLSn2JB2P/EAvbb5J2iPVZj3SppgzhwVCgYUu+osA3LNC4NsYxm/yMs8mq7nOCIZd6D/BM9py5WKS6//e4mM6sY3/S2wOr8snkUsEuu5M35zyRcrCjIaRzV9OKZjP+aqkk2GcF/Figd3N/zCZ+WjC+L9r/ELHn64qEJxZDvXKXVE3dUOchbUPelCb3YO+Mub+76bnvt8IQ2MRf9NaFO7cWlh9mDWkZMXxmOTlxOxQtOeTrW+QywTkZaDGkP83HRjqXd7bn3YBcdFiOy/
此处使用 CallbackUrlKey:"TencentEssEncryptTestKey12345678" 解密后可获取以下明文:
!该 CallbackUrlKey 仅用于此处测试样例。
{
"FlowId": "yDRtrAAAAAAAAAAAAAAAAAAAAAAAAAAA",
"DocumentId": "yDRtrBBBBBBBBBBBBBBBBBBBBBBBBBB",
"CallbackType": "sign",
"FlowName": "测试流程",
"FlowDescription": "",
"FlowType": "",
"FlowCallbackStatus": 4,
"Unordered": true,
"CreateOn": 1658892449,
"UpdatedOn": 1659604019,
"DeadLine": 1661615999,
"UserId": "",
"RecipientId": "yDRtrCCCCCCCCCCCCCCCCCCCCCCCCCCC",
"Operate": "sign",
"UserData": "",
"Approvers": [
{
"UserId": "yDRtrDDDDDDDDDDDDDDDDDDDDDDDDDDD",
"RecipientId": "yDRtrCCCCCCCCCCCCCCCCCCCCCCCCCCC",
"ApproverType": 1,
"OrganizationName": "",
"Required": true,
"ApproverName": "张三",
"ApproverMobile": "15912345678",
"ApproverIdCardType": "ID_CARD",
"ApproverIdCardNumber": "440300200101010001",
"ApproveCallbackStatus": 3,
"ApproveMessage": "",
"ApproveTime": 1659604019,
"VerifyChannel": "WEIXINAPP"
}
],
"CallbackUrl": "https://www.esstest.com"
}
回调解密 demo
Java
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
public class CallbackAes {
public static byte[] pkcs7Padding(byte[] ciphertext, int blockSize) {
int padding = blockSize - ciphertext.length % blockSize;
byte[] padtext = repeat((byte) padding, padding);
ciphertext = append(ciphertext, padtext);
return ciphertext;
}
public static byte[] repeat(byte val, int count) {
byte[] result = new byte[count];
for (int i = 0; i < count; i++) {
result[i] = val;
}
return result;
}
public static byte[] append(byte[] a, byte[] b) {
byte[] result = new byte[a.length + b.length];
System.arraycopy(a, 0, result, 0, a.length);
System.arraycopy(b, 0, result, a.length, b.length);
return result;
}
public static byte[] pkcs7UnPadding(byte[] origData) {
int length = origData.length;
int unpadding = origData[length - 1];
byte[] result = new byte[length - unpadding];
System.arraycopy(origData, 0, result, 0, result.length);
return result;
}
public static byte[] aesEncrypt(byte[] origData, byte[] key) throws Exception {
Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding");
int blockSize = cipher.getBlockSize();
origData = pkcs7Padding(origData, blockSize);
SecretKeySpec keyspec = new SecretKeySpec(key, "AES");
byte[] iv = new byte[blockSize];
System.arraycopy(key, 0, iv, 0, iv.length);
IvParameterSpec ivspec = new IvParameterSpec(iv);
cipher.init(Cipher.ENCRYPT_MODE, keyspec, ivspec);
byte[] encrypted = cipher.doFinal(origData);
return Base64.getEncoder().encode(encrypted);
}
public static byte[] aesDecrypt(byte[] crypted, byte[] key) throws Exception {
byte[] decoded = Base64.getDecoder().decode(crypted);
Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding");
int blockSize = cipher.getBlockSize();
SecretKeySpec keyspec = new SecretKeySpec(key, "AES");
byte[] iv = new byte[blockSize];
System.arraycopy(key, 0, iv, 0, iv.length);
IvParameterSpec ivspec = new IvParameterSpec(iv);
cipher.init(Cipher.DECRYPT_MODE, keyspec, ivspec);
byte[] origData = cipher.doFinal(decoded);
return pkcs7UnPadding(origData);
}
public static void main(String[] args) throws Exception {
// 传入CallbackUrlKey
byte[] key = "***************".getBytes();
// 传入密文
byte[] origData = aesDecrypt("****************".getBytes(StandardCharsets.UTF_8), key);
// 打印解密后的内容,格式为json
System.out.println(new String(origData, StandardCharsets.UTF_8));
}
}
PHP
<?php
require_once __DIR__.'/../../../vendor/autoload.php';
class Aes
{
public $key = '';
public $iv = '';
public function __construct($config)
{
foreach($config as $k => $v){
$this->$k = $v;
}
}
//解密
public function aesDe($data){
return openssl_decrypt(base64_decode($data), $this->method, $this->key, OPENSSL_RAW_DATA, $this->key);
}
}
$config = [
'key' => '********************', // 此处填入CallbackUrlKey
'method' => 'AES-256-CBC' //加密方式
];
$obj = new Aes($config);
// 此处填入收到的密文
$data = '*****************************';
echo $obj->aesDe($data);//解密
Golang
package v20201111
import (
"crypto/aes"
"crypto/cipher"
"encoding/base64"
"fmt"
"testing"
)
func AesDecrypt(crypted, key []byte) ([]byte, error) {
block, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
blockSize := block.BlockSize()
blockMode := cipher.NewCBCDecrypter(block, key[:blockSize])
origData := make([]byte, len(crypted))
blockMode.CryptBlocks(origData, crypted)
origData = PKCS7UnPadding(origData)
return origData, nil
}
// PKCS7UnPadding 去除填充
func PKCS7UnPadding(origData []byte) []byte {
length := len(origData)
unPadding := int(origData[length-1])
return origData[:(length - unPadding)]
}
func TestDecrypt(t *testing.T) {
// 传入CallbackUrlKey
key := "***********"
// 传入密文
content := "***********"
// base64解密
crypted, err := base64.StdEncoding.DecodeString(content)
if err != nil {
fmt.Printf("base64 DecodeString returned: %s", err)
return
}
origData, err := AesDecrypt(crypted, []byte(key))
if err != nil {
fmt.Printf("AesDecrypt returned: %s", err)
return
}
fmt.Printf("%s", string(origData))
}
::: ::: Python
# -*- coding: utf-8 -*-
import base64
from Cryptodome.Cipher import AES
def decode_aes256(data, encryption_key):
iv = encryption_key[0:16]
aes = AES.new(encryption_key, AES.MODE_CBC, iv)
d = aes.decrypt(data)
unpad = lambda s: s[0:-ord(d[-1:])]
return unpad(d)
# 此处传入密文
data = '**************************************************'
data = base64.b64decode(data)
# 此处传入CallbackUrlKey
e = decode_aes256(data, bytes('**************************************************', encoding="utf8"))
print(type(e))
print(str(e, encoding="utf8"))
C#
using System;
using System.Security.Cryptography;
using System.Text;
namespace TencentCloudExamples
{
class EssCallback
{
static void Main1(string[] args)
{
try
{
// 传入CallbackUrlKey
String key = "*************";
// 传入密文
String content = ""*************";";
String plaintext = AESDecrypt(content, Encoding.ASCII.GetBytes(key));
Console.WriteLine(plaintext);
}
catch (Exception e)
{
Console.WriteLine(e.ToString());
}
Console.Read();
}
public static string AESDecrypt(string encryptStr, byte[] key)
{
byte[] toEncryptArray = Convert.FromBase64String(encryptStr);
RijndaelManaged rDel = new RijndaelManaged();
rDel.Key = key;
byte[] iv = new byte[16];
Array.Copy(key, iv, iv.Length);
rDel.IV = iv;
rDel.Mode = CipherMode.CBC;
rDel.Padding = PaddingMode.PKCS7;
ICryptoTransform cTransform = rDel.CreateDecryptor();
byte[] resultArray = cTransform.TransformFinalBlock(toEncryptArray, 0, toEncryptArray.Length);
return UTF8Encoding.UTF8.GetString(resultArray);
}
}
}