Chaindoc webhooks

Los webhooks envían datos de eventos al servidor en el momento exacto en que ocurre algo en Chaindoc. Sin polling, sin esperas. Aquí está el truco: esta guía cubre la configuración, tipos de eventos, verificación HMAC, lógica de reintentos y pruebas.

Visión general

En lugar de hacer polling constante al API, los webhooks avisan al servidor qué pasó justo cuando sucede. Los usarás para sincronizar el estado de documentos, activar flujos de trabajo después de firmas, enviar notificaciones y mantener tu base de datos actualizada.

  • Entrega instantánea con hasta 3 reintentos automáticos (backoff exponencial)
  • Verificación de firma HMAC SHA256 en cada payload
  • Filtra únicamente los tipos de eventos que te importan
  • Seguimiento del estado de entrega en tu panel

Configuración

Paso 1: Crear una API key

Navega a Configuración → Acceso API en tu panel de Chaindoc y crea una API key con la configuración de webhooks habilitada.

Paso 2: Configurar la URL del webhook

Usa el API para configurar tu endpoint de 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"
  }'

Paso 3: Implementar tu endpoint

Crea un endpoint en tu servidor para recibir eventos de webhook. Aquí tienes ejemplos en diferentes lenguajes:

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);

Tipos de eventos

Chaindoc envía webhooks para los siguientes eventos:

document.created

Se activa cuando se crea un nuevo documento vía API.

terminal
{
  "event": "document.created",
  "documentId": "86840ee4-8bf2-4a91-a289-e99d8307ec25",
  "name": "Service Agreement",
  "timestamp": "2024-12-04T10:30:00.000Z"
}

document.verified

Se activa cuando un documento se verifica correctamente en 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

Se activa cuando se recogen todas las firmas requeridas.

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

Se activa cuando se crea una nueva solicitud de firma.

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

Se activa cuando todos los firmantes completan sus firmas.

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

Se activa cuando un firmante rechaza la solicitud de firma.

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

Seguridad

Verificación de firma

Chaindoc firma todos los payloads de webhook usando HMAC SHA256. Siempre verifica las firmas para asegurar la autenticidad y prevenir ataques de repetición.

Cómo funciona la verificación

1Chaindoc crea la firmaChaindoc genera una firma HMAC usando tu secreto de webhook

2Firma enviada en el headerLa firma se envía en el header X-Webhook-Signature

3Tu servidor verificaTu servidor recalcula la firma y compara usando una función segura contra timing attacks

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');
}

Lógica de reintentos

Chaindoc reintenta automáticamente las entregas fallidas de webhooks con backoff exponencial.

  • 1er reintento: Después de 1 minuto
  • 2do reintento: Después de 5 minutos (total: 6 minutos)
  • 3er reintento: Después de 15 minutos (total: 21 minutos)

Pruebas de webhooks

Desarrollo local

Usa herramientas como ngrok para exponer tu servidor local y probar webhooks:

terminal
# Instalar ngrok
npm install -g ngrok

# Iniciar tu servidor local
node server.js

# Exponer el puerto 3000
ngrok http 3000

# Usa la URL de ngrok como endpoint de webhook
# Ejemplo: https://abc123.ngrok.io/webhooks/chaindoc

Pruebas manuales

Prueba tu endpoint de webhook con un payload de ejemplo:

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

Mejores prácticas

  • Siempre verifica las firmas de webhook antes de procesar
  • Responde rápido (menos de 30 segundos) para evitar timeouts
  • Procesa los webhooks de forma asíncrona en una cola
  • Implementa idempotencia para manejar eventos duplicados
  • Registra todos los eventos de webhook para depuración y auditoría
  • Monitorea los fallos de entrega de webhooks en tu panel
  • Usa endpoints HTTPS por seguridad
  • Maneja todos los tipos de eventos con elegancia (ignora eventos desconocidos)

Ejemplo de producción

Aquí tienes un manejador de webhooks listo para producción con integración de base de datos:

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

async function handleSignatureCompleted(payload: any) {
  await prisma.signatureRequest.update({
    where: { id: payload.signatureRequestId },
    data: {
      status: 'COMPLETED',
      completedAt: new Date(),
    },
  });
}

async function sendNotificationEmail(payload: any) {
  // Send email notification
}

function verifySignature(payload: any, signature: string, secret: string) {
  const hmac = crypto.createHmac('sha256', secret);
  const digest = hmac.update(JSON.stringify(payload)).digest('hex');
  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(digest)
  );
}

Qué hacer a continuación