腾讯电子签回调通知能力_办公协同解决方案
功能介绍
通过回调通知,您可以在合同状态产生变化以及发生印章、模板、员工相关操作等场景接收到来自电子签的回调通知。如您需要使用此功能,需提供能接受并处理 HTTP POST 请求的回调地址,请确保该地址提供的服务可公网访问并能正常处理回调通知。电子签推送的回调内容有进行加密处理,在接收到回调通知后需用配置好的 Key 进行解密。
配置回调地址
登录 腾讯电子签控制台,在企业应用管理中,选择应用集成。
单击生产运行或测试联调(如果当前为测试环境,请选择测试联调)。
在参数配置中设置回调地址,请确保填入的地址能够接受并处理 HTTP POST 请求,并返回状态码200表示处理正常。选择点击系统生成可以随机生成解密的Key。此处支持配置多个回调地址并同时生效。
支持的回调场景
详细回调说明及场景参见 回调通知能力。合同发起以及签署相关回调合同发起回调合同签署回调签署流程签署完成回调签署流程过期回调签署流程撤销回调签署流程拒签回调签署流程填写回调印章相关回调创建印章回调删除印章回调停用印章回调启用印章回调印章授权回调解除印章授权回调印章审核通过回调印章审核驳回回调模板相关回调创建模板回调编辑模板回调员工相关回调员工认证成功回调员工变更角色回调扣费相关回调合同扣费回调注意各类型回调的数据结构并不完全相同,请进行区分处理。
回调解密步骤
1. 对收到的数据进行 Base64 解码得到密文。2. 对密文进行对称解密,算法为 AES-256-CBC,密钥为腾讯电子签提供的 CallbackUrlKey,IV 取 CallbackUrlKey 值的前16位,数据采用 PKCS#7 填充。3. 解密得到的数据为输入参数的 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.1Host: www.esstest.comUser-Agent: Go-http-client/1.1Content-Length: 1088Content-Type: text/plainAccept-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
JavaPHPGolangPythonC#
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)); }}
<?phprequire_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);//解密
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))}
# -*- 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)# 此处传入CallbackUrlKeye = decode_aes256(data, bytes('**************************************************', encoding="utf8"))print(type(e))print(str(e, encoding="utf8"))
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); }
}
}
对腾讯办公协同的解决方案有疑惑?想了解解决方案收费? 联系解决方案专家
腾讯云限时活动1折起,即将结束: 马上收藏
同尘科技为腾讯云授权服务中心,购买腾讯云享受折上折,更有现金返利:同意关联,立享优惠
阿里云解决方案也看看?: 点击对比阿里云的解决方案