跳到主要内容

回调通知

功能介绍

2023年5月15日之后注册的企业,将默认使用新回调通知,请参考新回调通知

若客户主动选择新回调,请与客户经理联系处理

通过回调通知,您可以在合同状态产生变化以及发生印章、模板、员工相关操作等场景接受到来自电子签的回调通知。

如您需要使用此功能,需提供能接受并处理 HTTP POST 请求的回调地址,请确保该地址提供的服务可公网访问并能正常处理回调通知。

电子签推送的回调内容有进行加密处理,在接受到回调通知后需用配置好的 Key 进行解密。

配置回调地址

登录 腾讯电子签控制台集成管理中,选择生产运行(如果当前为测试环境,请选择测试联调)。 参数配置中设置回调地址,请确保填入的地址能够接受并处理 HTTP POST 请求,并返回状态码200表示处理正常。选择点击系统生成可以随机生成解密的Key。此处支持配置多个回调地址并同时生效。

支持的回调场景

  • 合同发起以及签署相关回调
    • 合同发起回调
    • 签署人签署回调
    • 签署流程签署完成回调
    • 签署流程过期回调
    • 签署流程撤销回调
    • 签署流程拒签回调
    • 签署流程填写回调
  • 印章相关回调
    • 创建印章回调
    • 删除印章回调
    • 停用印章回调
    • 启用印章回调
    • 印章授权回调
    • 解除印章授权回调
    • 印章审核通过回调
    • 印章审核驳回回调
  • 模板相关回调
    • 创建模板回调
    • 编辑模板回调
  • 员工相关回调
    • 员工认证成功回调
    • 员工变更角色回调
  • 扣费相关回调
    • 合同扣费回调

!各类型回调的数据结构并不完全相同,请进行区分处理。

回调解密步骤

  1. 对收到的数据进行 Base64 解码得到密文。
  2. 对密文进行对称解密,算法为 AES-256-CBC,密钥为腾讯电子签提供的 CallbackUrlKey,IV 取 CallbackUrlKey 值的前16位,数据采用 PKCS#7 填充。
  3. 解密得到的数据为输入参数的 Json 格式。

合同发起以及签署回调通知参数

回调数据对象 FlowInfo 结构

参数名称参数类型参数描述
FlowIdstring流程编号。
DocumentIdstring使用的文档 ID。
CallbackTypestring 回调的类型:
sign:签署回调
review:审核回调
fill:填写回调
FlowNamestring流程名称。
FlowTypestring流程的类型。
FlowDescriptionstring流程的描述。
Unorderedbool流程类型顺序:
true:为无序
false:为有序
CreateOnint流程的创建时间戳。
UpdatedOnint流程的修改时间戳。
DeadLineint流程的过期时间0为永远不过期。
FlowCallbackStatusint流程现在的状态:
1:待签署
2:部分签署
3:已拒签
4:已签署
5:已过期
6:已撤销
8:等待填写
21:已解除
UserIdstring本环节需要操作人 UserId。
RecipientIdstring签署区 ID。
Operate string动作:
start:发起
sign:签署
reject:拒签
cancel:取消
finish:结束
deadline:过期
fill:填写
UserDatastring创建的时候设置的透传字段。
ApproversApprover 数组流程签约方列表。

FlowInfo 参数 Approver 结构

参数名称参数类型参数描述
UserIdstring本环节需要操作人的 UserId。
RecipientIdstring签署区 ID。
ApproverTypeint参与者类型:
0:企业
1:个人
3:企业静默签署
OrganizationNamestring企业名称。
Requiredbool是否需要签名。
ApproverNamestring本环节需要操作人的名字。
ApproverMobilestring本环节需要操作人的手机号。
ApproverIdCardTypestring签署人证件类型:
ID_CARD:身份证。
HONGKONG_AND_MACAO:港澳居民来往内地通行证。
HONGKONG_MACAO_AND_TAIWAN:港澳台居民居住证(格式同居民身份证)。
ApproverIdCardNumberstring签署人证件号码。
ApproveCallbackStatusint签署状态:
2:待签署
3:已签署
4:已拒绝
5:已过期
6:已撤销
8:待填写
9:因为各种原因而终止
10:填写
15:已解除
19:已转他人处理
ApproveMessagestring拒签的原因。
VerifyChannelstring签署意愿方式,WEIXINAPP:人脸识别。
ApproveTimeint签约的时间。

印章回调通知参数

回调数据对象 CallbackContent 结构

参数名称参数类型参数描述
MsgIdstring消息编号。
MsgTypestring消息类型,固定为 OperateSeal。
MsgDataMsgData 消息数据。

CallbackContent 参数 MsgData 结构

印章共有创建、删除、停用、启用、印章授权、解除印章授权、印章审核通过、印章审核驳回共8种回调。为方便描述区分,此处将印章审核通过、印章审核驳回归为“审核结果回调”类型,将其他回调归为“非审核结果回调”类型。

参数名称参数类型参数描述
OrganizationIdstring企业 ID。仅在非审核结果回调时有值。
OperatorUserIdstring操作人 UserId。仅在非审核结果回调时有值。
SealIdstring 印章 ID。
SealNamestring 印章名称。
Operatestring动作,仅在非审核结果回调时有值:
Create:创建
Delete:删除
Disable:停用
Enable:启用
Valid:印章授权
Invalid:解除印章授权
AuthorizedUsersAuthorizedUser 数组被授权用户数组,仅在 Operate 为 Enable,Disable 时有值。
ReviewStatusstring审核状态,仅在审核结果回调时有值:
PASS:通过
REJECT:拒绝
ReviewReasonstring审核通过或失败原因,仅在审核结果回调时有值。

MsgData 参数 AuthorizedUser 结构

参数名称参数类型参数描述
Idstring被授权用户 ID。
Namestring被授权用户姓名。

模板回调通知参数

回调数据对象 CallbackContent 结构

参数名称参数类型参数描述
MsgIdstring 消息编号。
MsgTypestring消息类型:
TemplateAdd:创建模板
TemplateUpdate:编辑模板
MsgDataMsgData消息数据。

CallbackContent 参数 MsgData 结构

参数名称参数类型参数描述
OrganizationIdstring 企业 ID。
OperatorUserIdstring操作人 UserId。
TemplateIdstring 模板 ID。
TemplateNamestring 模板名称。
CreateTimeint创建时间,秒级时间戳,仅在创建模板时有值。
UpdateTimeint 更新时间,秒级时间戳,仅在编辑模板时有值。

员工回调通知参数

回调数据对象 CallbackContent 结构

参数名称参数类型参数描述
MsgIdstring消息编号。
MsgTypestring 消息类型:
VerifyStaffInfo:员工认证成功
RolesChange:员工变更角色
MsgDataMsgData消息数据。

CallbackContent 参数 MsgData 结构

参数名称参数类型参数描述
OrganizationIdstring 企业 ID。
UserIdstring员工 UserId。
Namestring 员工姓名。
BeforeRoleNames string 数组 变更前角色名数组,仅在员工变更角色时有值。
AfterRoleNames string 数组 变更后角色名数组,仅在员工变更角色时有值。

扣费回调通知参数

回调数据对象 CallbackContent 结构

参数名称参数类型参数描述
MsgIdstring消息编号。
MsgTypestring 消息类型:
FlowCost:合同扣费。
MsgDatamsgdata消息数据。

CallbackContent 参数 MsgData 结构

参数名称参数类型参数描述
Costint扣除份额,如果为负值则为份额退还。
CostChannelstring 扣费渠道。
FlowIdstring 合同 ID。
OrganizationIdstring 企业 ID。
IsResellbool分销订单标识。

回调 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);
}

}

}