Chaindoc logoChaindoc

Вебхуки Chaindoc

Вебхуки надсилають дані про події на ваш сервер у той самий момент, коли щось відбувається в Chaindoc. Без опитування, без затримок. Тут розповідається про налаштування, типи подій, HMAC-верифікацію, логіку повторних спроб і тестування.

Огляд

Замість постійного опитування API, вебхуки миттєво повідомляють ваш сервер про події. Ви використовуватимете їх для синхронізації статусу документів, запуску робочих процесів після підписів, відправки сповіщень і підтримки актуальності вашої бази даних.

  • Миттєва доставка з до 3 автоматичних повторних спроб (експоненціальне відкладання)
  • Перевірка підпису HMAC SHA256 для кожного payload
  • Фільтрація лише потрібних типів подій
  • Відстеження статусу доставки в вашій панелі керування

Налаштування

Крок 1: Створення API ключа

Перейдіть до Settings → API Access у панелі керування Chaindoc і створіть API ключ з увімкненою конфігурацією вебхуків.

Крок 2: Конфігурація URL вебхука

Використовуйте API для налаштування вашого webhook endpoint:

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

Крок 3: Реалізація вашого endpoint

Створіть endpoint на вашому сервері для отримання 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 надсилає вебхуки для таких подій:

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 підписує всі webhook-payload за допомогою HMAC SHA256. Завжди перевіряйте підписи для забезпечення автентичності та запобігання атакам повторного відтворення.

Як працює перевірка

1Chaindoc створює підписChaindoc створює HMAC підпис за допомогою вашого webhook secret

2Підпис надсилається в заголовкуПідpis надсилається в заголовку X-Webhook-Signature

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 автоматично повторює невдалі доставки вебхуків з експоненціальним відкладанням.

  • 1-ша спроба: через 1 хвилину
  • 2-га спроба: через 5 хвилин (всього: 6 хвилин)
  • 3-тя спроба: через 15 хвилин (всього: 21 хвилина)

Тестування вебхуків

Локальна розробка

Використовуйте інструменти на кшталт ngrok для відкриття доступу до вашого локального сервера для тестування вебхуків:

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

Ручне тестування

Протестуйте ваш webhook endpoint з прикладом 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"
  }'

Найкращі практики

  • Завжди перевіряйте підписи вебхуків перед обробкою
  • Відповідайте швидко (менше 30 секунд) для уникнення таймаутів
  • Обробляйте вебхуки асинхронно в черзі
  • Реалізуйте ідемпотентність для обробки дублікатів подій
  • Логуйте всі webhook-події для налагодження та аудиту
  • Моніторте невдалі доставки вебхуків у вашій панелі керування
  • Використовуйте HTTPS endpoints для безпеки
  • Обробляйте всі типи подій коректно (ігноруйте невідомі події)

Приклад для продакшену

Ось готовий до продакшену обробник вебхуків з інтеграцією бази даних:

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(),
    },
  });
}

Що робити далі