Chaindoc webhooks使用指南
Webhooks能在Chaindoc发生事件的瞬间将数据推送到你的服务器。无需轮询,没有延迟。本文涵盖设置步骤、事件类型、HMAC验证、重试逻辑和测试方法。
使用要求
Business Plan订阅(仅商业版用户可配置webhooks) 服务器上的HTTPS端点 可公开访问的URL(webhooks无法发送到localhost)
概述
其实不需要轮询API,webhooks会在事件发生时立即通知你的服务器。你可以用它同步文档状态、签名完成后触发工作流、发送通知,并保持数据库同步。
- 即时投递,最多3次自动重试(指数退避)
- 每个payload都有HMAC SHA256签名验证
- 只接收你关心的事件类型
- 在控制台中跟踪投递状态
设置步骤
第一步:创建API密钥
进入Chaindoc控制台的设置→API访问页面,创建一个启用webhook配置的API密钥。
第二步:配置webhook URL
使用API配置你的webhook端点:
terminal
curl -X PATCH https://api.chaindoc.io/user/api-access/1/config \
-H "Authorization: Bearer your_auth_token" \
-H "Content-Type: application/json" \
-d '{
"webhookUrl": "https://yourapp.com/webhooks/chaindoc",
"webhookEnabled": true,
"webhookSecret": "your_secure_random_string"
}'第三步:实现端点
在你的服务器上创建接收webhook事件的端点。以下是不同语言的示例代码:
const express = require('express');
const crypto = require('crypto');
const app = express();
app.use(express.json());
app.post('/webhooks/chaindoc', (req, res) => {
const signature = req.headers['x-webhook-signature'];
const eventType = req.headers['x-webhook-event'];
// Verify signature
if (!verifySignature(req.body, signature, process.env.WEBHOOK_SECRET)) {
return res.status(401).send('Invalid signature');
}
// Process event
console.log('Received event:', eventType, req.body);
// Handle different event types
switch (eventType) {
case 'document.created':
handleDocumentCreated(req.body);
break;
case 'document.verified':
handleDocumentVerified(req.body);
break;
case 'signature.request.completed':
handleSignatureCompleted(req.body);
break;
}
// Always respond with 200 OK
res.status(200).send('Webhook received');
});
function verifySignature(payload, signature, secret) {
const hmac = crypto.createHmac('sha256', secret);
const digest = hmac.update(JSON.stringify(payload)).digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(digest)
);
}
app.listen(3000);事件类型
Chaindoc为以下事件发送webhooks:
document.created
通过API创建新文档时触发。
terminal
{
"event": "document.created",
"documentId": "86840ee4-8bf2-4a91-a289-e99d8307ec25",
"name": "Service Agreement",
"timestamp": "2024-12-04T10:30:00.000Z"
}document.verified
文档成功上链验证时触发。
terminal
{
"event": "document.verified",
"documentId": "86840ee4-8bf2-4a91-a289-e99d8307ec25",
"versionId": "f0b7721f-0399-4035-9b69-7b95d3a367f0",
"txHash": "0x789ghi...",
"chainId": 137,
"timestamp": "2024-12-04T10:35:00.000Z"
}document.signed
收集完所有必需签名时触发。
terminal
{
"event": "document.signed",
"documentId": "86840ee4-8bf2-4a91-a289-e99d8307ec25",
"signatureRequestId": "req_21096b94498f4a2d9795e810edc2c9a9",
"signers": [
{
"email": "signer1@example.com",
"signedAt": "2024-12-04T10:30:00.000Z"
},
{
"email": "signer2@example.com",
"signedAt": "2024-12-04T10:32:00.000Z"
}
],
"timestamp": "2024-12-04T10:32:00.000Z"
}signature.request.created
创建新签名请求时触发。
terminal
{
"event": "signature.request.created",
"signatureRequestId": "req_21096b94498f4a2d9795e810edc2c9a9",
"documentId": "86840ee4-8bf2-4a91-a289-e99d8307ec25",
"recipients": [
{"email": "signer1@example.com"},
{"email": "signer2@example.com"}
],
"deadline": "2024-12-31T23:59:59.000Z",
"timestamp": "2024-12-04T10:30:00.000Z"
}signature.request.completed
所有签署者完成签名时触发。
terminal
{
"event": "signature.request.completed",
"signatureRequestId": "req_21096b94498f4a2d9795e810edc2c9a9",
"documentId": "86840ee4-8bf2-4a91-a289-e99d8307ec25",
"completedAt": "2024-12-04T10:32:00.000Z",
"timestamp": "2024-12-04T10:32:00.000Z"
}signature.request.rejected
签署者拒绝签名请求时触发。
terminal
{
"event": "signature.request.rejected",
"signatureRequestId": "req_21096b94498f4a2d9795e810edc2c9a9",
"documentId": "86840ee4-8bf2-4a91-a289-e99d8307ec25",
"rejectedBy": "signer1@example.com",
"reason": "Terms not acceptable",
"timestamp": "2024-12-04T10:30:00.000Z"
}安全验证
签名验证
说实话,Chaindoc使用HMAC SHA256对所有webhook payload进行签名。始终验证签名以确保真实性并防止重放攻击。
安全提示
使用定时安全比较函数 生产环境绝不能跳过签名验证 妥善保管webhook密钥 定期轮换密钥 仅使用HTTPS端点
验证原理
1Chaindoc创建签名Chaindoc使用你的webhook密钥创建HMAC签名
2通过header发送签名签名通过X-Webhook-Signature header发送
3你的服务器进行验证你的服务器重新计算签名并使用定时安全函数进行比对
const crypto = require('crypto');
function verifyWebhookSignature(payload, signature, secret) {
const hmac = crypto.createHmac('sha256', secret);
const digest = hmac.update(JSON.stringify(payload)).digest('hex');
// Use timing-safe comparison to prevent timing attacks
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(digest)
);
}
// Usage
const isValid = verifyWebhookSignature(
req.body,
req.headers['x-webhook-signature'],
process.env.WEBHOOK_SECRET
);
if (!isValid) {
return res.status(401).send('Invalid signature');
}重试机制
Chaindoc会自动对失败的webhook投递进行指数退避重试。
- 第1次重试:1分钟后
- 第2次重试:5分钟后(累计:6分钟)
- 第3次重试:15分钟后(累计:21分钟)
成功的标准
端点返回HTTP状态码200-299 端点在30秒内响应 投递过程中没有网络错误
测试webhooks
本地开发
使用ngrok等工具暴露本地服务器进行webhook测试:
terminal
# Install ngrok
npm install -g ngrok
# Start your local server
node server.js
# Expose port 3000
ngrok http 3000
# Use the ngrok URL as your webhook endpoint
# Example: https://abc123.ngrok.io/webhooks/chaindoc手动测试
使用示例payload测试你的webhook端点:
terminal
curl -X POST https://yourapp.com/webhooks/chaindoc \
-H "Content-Type: application/json" \
-H "X-Webhook-Event: document.created" \
-H "X-Webhook-Signature: test_signature" \
-d '{
"event": "document.created",
"documentId": "test-123",
"name": "Test Document",
"timestamp": "2024-12-04T10:30:00.000Z"
}'最佳实践
- 处理前始终验证webhook签名
- 快速响应(30秒内)避免超时
- 在队列中异步处理webhooks
- 实现幂等性以处理重复事件
- 记录所有webhook事件用于调试和审计
- 在控制台监控webhook投递失败
- 使用HTTPS端点确保安全
- 优雅处理所有事件类型(忽略未知事件)
生产环境示例
这是一个带数据库集成的生产级webhook处理器:
webhooks/chaindoc.ts
import express from 'express';
import crypto from 'crypto';
import { PrismaClient } from '@prisma/client';
const app = express();
const prisma = new PrismaClient();
app.use(express.json());
app.post('/webhooks/chaindoc', async (req, res) => {
const signature = req.headers['x-webhook-signature'] as string;
const eventType = req.headers['x-webhook-event'] as string;
const payload = req.body;
// 1. Verify signature
if (!verifySignature(payload, signature, process.env.WEBHOOK_SECRET!)) {
console.error('Invalid webhook signature');
return res.status(401).json({ error: 'Invalid signature' });
}
// 2. Check for duplicate events (idempotency)
const eventId = `${eventType}-${payload.timestamp}`;
const existing = await prisma.webhookEvent.findUnique({
where: { eventId },
});
if (existing) {
console.log('Duplicate event, skipping:', eventId);
return res.status(200).json({ status: 'duplicate' });
}
// 3. Store event
await prisma.webhookEvent.create({
data: {
eventId,
eventType,
payload,
processedAt: new Date(),
},
});
// 4. Process event asynchronously
processWebhookAsync(eventType, payload).catch((error) => {
console.error('Error processing webhook:', error);
});
// 5. Respond immediately
res.status(200).json({ status: 'received' });
});
async function processWebhookAsync(eventType: string, payload: any) {
switch (eventType) {
case 'document.verified':
await handleDocumentVerified(payload);
break;
case 'signature.request.completed':
await handleSignatureCompleted(payload);
await sendNotificationEmail(payload);
break;
case 'signature.request.rejected':
await handleSignatureRejected(payload);
break;
}
}
async function handleDocumentVerified(payload: any) {
await prisma.document.update({
where: { id: payload.documentId },
data: {
status: 'verified',
txHash: payload.txHash,
verifiedAt: new Date(),
},
});
}