Chaindoc logoChaindoc

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 , 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:

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"
  }'

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.

terminal
{
  "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.

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

Kích hoạt khi tất cả chữ ký cần thiết đã được thu thập.

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

Kích hoạt khi yêu cầu ký mới được tạo.

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

Kích hoạt khi tất cả ngườù ký hoàn tất chữ ký của họ.

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

Kích hoạt khi ngườù ký từ chối yêu cầu ký.

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"
}

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:

terminal
# 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/chaindoc

Test thủ công

Test webhook endpoint của bạn với sample payload:

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"
  }'

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:

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: { 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