Chaindoc webhooks
Webhooks đẩy dữ liệu sự kiện đến server của bạn ngay lập tức khi có việc gì đó xảy ra trong Chaindoc. Không polling, không delay. Trang này sẽ hướng dẫn cách thiết lập, các loại sự kiện, xác minh HMAC, logic retry và cách test.
Tổng quan
Thay vì polling API, webhooks sẽ báo cho server của bạn biết điều gì vừa xảy ra ngay khi nó xảy ra. Bạn dùng chúng để đồng bộ trạng thái tài liệu, trigger workflow sau khi ký, gửi thông báo và giữ database luôn sync.
- Gửi ngay lập tức với tối đa 3 lần retry tự động (exponential backoff)
- Xác minh chữ ký HMAC SHA256 trên mọi payload
- Lọc chỉ những loại sự kiện bạn quan tâm
- Theo dõi trạng thái gửi trong dashboard
Thiết lập
Bước 1: Tạo API key
Vào Settings → API Access trong Chaindoc dashboard và tạo API key với webhook configuration được bật.
Bước 2: Cấu hình webhook URL
Dùng API để cấu hình webhook endpoint của bạn:
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"
}'Bước 3: Triển khai endpoint
Tạo endpoint trên server của bạn để nhận sự kiện webhook. Dưới đây là ví dụ bằng các ngôn ngữ khác nhau:
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);Các loại sự kiện
Chaindoc gửi webhooks cho các sự kiện sau:
document.created
Kích hoạt khi tài liệu mới được tạo qua API.
{
"event": "document.created",
"documentId": "86840ee4-8bf2-4a91-a289-e99d8307ec25",
"name": "Service Agreement",
"timestamp": "2024-12-04T10:30:00.000Z"
}document.verified
Kích hoạt khi tài liệu được xác minh thành công trên blockchain.
{
"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
Kích hoạt khi tất cả chữ ký cần thiết đã được thu thập.
{
"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
Kích hoạt khi yêu cầu ký mới được tạo.
{
"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
Kích hoạt khi tất cả ngườù ký hoàn tất chữ ký của họ.
{
"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
Kích hoạt khi ngườù ký từ chối yêu cầu ký.
{
"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"
}Bảo mật
Xác minh chữ ký
Chaindoc ký tất cả webhook payloads bằng HMAC SHA256. Luôn xác minh chữ ký để đảm bảo tính xác thực và ngăn chặn replay attacks.
Cách xác minh hoạt động
1Chaindoc tạo chữ kýChaindoc tạo chữ ký HMAC bằng webhook secret của bạn
2Chữ ký gửi trong headerChữ ký được gửi trong header X-Webhook-Signature
3Server của bạn xác minhServer của bạn tính lại chữ ký và so sánh bằng timing-safe function
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');
}Logic retry
Chaindoc tự động retry các webhook delivery thất bại với exponential backoff.
- Retry lần 1: Sau 1 phút
- Retry lần 2: Sau 5 phút (tổng: 6 phút)
- Retry lần 3: Sau 15 phút (tổng: 21 phút)
Test webhooks
Phát triển local
Dùng công cụ như ngrok để expose server local của bạn cho việc test webhook:
# Cài ngrok
npm install -g ngrok
# Khởi động server local
node server.js
# Expose port 3000
ngrok http 3000
# Dùng ngrok URL làm webhook endpoint
# Ví dụ: https://abc123.ngrok.io/webhooks/chaindocTest thủ công
Test webhook endpoint của bạn với sample payload:
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"
}'Best practices
- Luôn xác minh webhook signatures trước khi xử lý
- Phản hồi nhanh (dưới 30 giây) để tránh timeout
- Xử lý webhooks bất đồng bộ trong queue
- Triển khai idempotency để xử lý sự kiện trùng lặp
- Log tất cả sự kiện webhook để debug và audit
- Giám sát webhook delivery failures trong dashboard
- Dùng HTTPS endpoints để bảo mật
- Xử lý tất cả loại sự kiện một cách graceful (bỏ qua sự kiện không xác định)
Ví dụ production
Dưới đây là webhook handler sẵn sàng cho production với database integration:
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: { verified: true, txHash: payload.txHash },
});
}
async function handleSignatureCompleted(payload: any) {
await prisma.signatureRequest.update({
where: { id: payload.signatureRequestId },
data: { status: 'completed', completedAt: payload.completedAt },
});
}
async function handleSignatureRejected(payload: any) {
await prisma.signatureRequest.update({
where: { id: payload.signatureRequestId },
data: { status: 'rejected', rejectedReason: payload.reason },
});
}
function verifySignature(payload: any, signature: string, secret: string): boolean {
const hmac = crypto.createHmac('sha256', secret);
const digest = hmac.update(JSON.stringify(payload)).digest('hex');
return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(digest));
}Làm gì tiếp theo
- API integration — các pattern phổ biến và ví dụ workflow
- API documentation — full endpoint reference
- SDKs — hướng dẫn Server SDK và Embed SDK
- Security — xác minh HMAC và quản lý key
- Installation — thiết lập SDK cho mọi framework