Chaindoc Webhooks
Receive real-time notifications when events occur in your Chaindoc account. Webhooks push event data to your server instantly, eliminating the need for polling.
Overview
Chaindoc Webhooks allow your application to receive real-time notifications when events occur in your account. Instead of polling the API, webhooks push event data to your server as soon as an action happens.
Key Features
- Real-time Notifications - Instant event delivery to your server
- Automatic Retries - Up to 3 retry attempts with exponential backoff
- Signature Verification - HMAC SHA256 for payload authenticity
- Event Filtering - Receive only the events you care about
- Error Tracking - Monitor webhook delivery status and failures
Use Cases
- Send email notifications when documents are signed
- Trigger workflows when documents are verified on blockchain
- Update your database when signature requests are created
- Sync document status with external systems
- Audit trail and compliance logging
Setup
Step 1: Create API Key
Navigate to Settings → API Access in your Chaindoc dashboard and create an API key with webhook configuration enabled.
Step 2: Configure Webhook URL
Use the API to configure your webhook endpoint:
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"
}'Step 3: Implement Webhook Endpoint
Create an endpoint on your server to receive webhook events. Here are examples in different languages:
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);Event Types
Chaindoc sends webhooks for the following events:
document.created
Triggered when a new document is created via API.
{
"event": "document.created",
"documentId": "86840ee4-8bf2-4a91-a289-e99d8307ec25",
"name": "Service Agreement",
"timestamp": "2024-12-04T10:30:00.000Z"
}document.verified
Triggered when a document is successfully verified on 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
Triggered when all required signatures are collected.
{
"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
Triggered when a new signature request is created.
{
"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
Triggered when all signers complete their signatures.
{
"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
Triggered when a signer rejects the signature request.
{
"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"
}Security
Signature Verification
Chaindoc signs all webhook payloads using HMAC SHA256. Always verify signatures to ensure authenticity and prevent replay attacks.
How Signature Verification Works
1Chaindoc Creates SignatureChaindoc creates HMAC signature using your webhook secret
2Signature Sent in HeaderSignature is sent in X-Webhook-Signature header
3Your Server VerifiesYour server recalculates signature and compares using 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');
}Retry Logic
Chaindoc automatically retries failed webhook deliveries with exponential backoff.
- 1st retry: After 1 minute
- 2nd retry: After 5 minutes (total: 6 minutes)
- 3rd retry: After 15 minutes (total: 21 minutes)
Testing Webhooks
Local Development
Use tools like ngrok to expose your local server for webhook testing:
# 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/chaindocManual Testing
Test your webhook endpoint with a 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
- Always verify webhook signatures before processing
- Respond quickly (under 30 seconds) to avoid timeouts
- Process webhooks asynchronously in a queue
- Implement idempotency to handle duplicate events
- Log all webhook events for debugging and audit
- Monitor webhook delivery failures in your dashboard
- Use HTTPS endpoints for security
- Handle all event types gracefully (ignore unknown events)
Complete Example
Here's a production-ready webhook handler with 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: {
status: 'verified',
blockchainTxHash: payload.txHash,
verifiedAt: new Date(payload.timestamp),
},
});
}
async function handleSignatureCompleted(payload: any) {
await prisma.signatureRequest.update({
where: { id: payload.signatureRequestId },
data: {
status: 'completed',
completedAt: new Date(payload.completedAt),
},
});
}
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));
}
app.listen(3000);