Custom Webhook Integration

Learn how to set up and use custom webhooks to receive real-time notifications when articles are published, updated, or deleted.

Custom Webhook Integration

Custom webhooks allow you to receive real-time notifications when articles are published, updated, or deleted in your site. This enables you to integrate your content publishing workflow with external systems, automation tools, or custom applications.

Overview

Webhooks are HTTP callbacks that send POST requests to your specified URL when specific events occur. Our webhook system supports:

  • Multiple event types: Subscribe to article published, updated, or deleted events
  • Flexible authentication: Bearer token, Basic auth, or no authentication
  • HMAC signature verification: Optional secret key for secure webhook verification
  • Automatic retries: Failed deliveries are automatically retried for server errors (5xx)
  • Delivery logging: All webhook deliveries are logged for debugging and monitoring

Setting Up a Webhook

Step 1: Navigate to Webhooks

  1. Go to your site's Settings page
  2. Navigate to the Integrations section
  3. Click on Webhooks

Step 2: Create a New Webhook

  1. Click the Create Webhook button
  2. Fill in the webhook configuration:
    • Name: A friendly name to identify this webhook (e.g., "Production CMS Webhook")
    • Webhook URL: The endpoint URL where webhook payloads will be sent (must be HTTPS)
    • Events: Select which events should trigger this webhook:
      • article.published - Triggered when an article is published
      • article.updated - Triggered when an article is updated
      • article.deleted - Triggered when an article is deleted
    • Authentication Method: Choose how to authenticate webhook requests:
      • None: No authentication (not recommended for production)
      • Bearer Token: Include a bearer token in the Authorization header
      • Basic Authentication: Use username and password for authentication
    • Secret Key (Optional): A secret key for generating HMAC SHA-256 signatures
    • Active: Toggle to enable or disable the webhook

Step 3: Test Your Webhook

After creating the webhook, you can test it using the Test Webhook button. This will send a sample payload to your endpoint to verify the configuration.

Webhook Payload

When an event occurs, a POST request is sent to your webhook URL with the following payload structure:

{
  "event_type": "article.published",
  "timestamp": "2024-01-15T10:30:00.000Z",
  "data": {
    "article_id": "550e8400-e29b-41d4-a716-446655440000",
    "site_id": "660e8400-e29b-41d4-a716-446655440000",
    "title": "Getting Started with SEO",
    "slug": "getting-started-with-seo",
    "status": "published",
    "content": {
      "body": "<p>Article content in HTML format...</p>",
      "html": "<p>Article content in HTML format...</p>",
      "markdown": "# Getting Started with SEO\n\nArticle content in markdown..."
    },
    "seo_data": {
      "meta_title": "Getting Started with SEO - Best Practices",
      "meta_description": "Learn the fundamentals of SEO...",
      "keywords": ["seo", "search engine optimization"]
    },
    "meta_description": "Learn the fundamentals of SEO and improve your website's visibility.",
    "featured_image": "https://example.com/images/featured.jpg",
    "published_at": "2024-01-15T10:30:00.000Z",
    "created_at": "2024-01-10T08:00:00.000Z",
    "updated_at": "2024-01-15T10:30:00.000Z",
    "author": {
      "id": "770e8400-e29b-41d4-a716-446655440000",
      "email": "author@example.com"
    }
  }
}

Payload Fields

FieldTypeDescription
event_typestringThe type of event: article.published, article.updated, or article.deleted
timestampstringISO 8601 timestamp when the webhook was triggered
data.article_idstringUnique identifier for the article
data.site_idstringUnique identifier for the site
data.titlestringArticle title
data.slugstringURL-friendly slug for the article
data.statusstringCurrent status of the article (e.g., "published", "draft")
data.content.bodystringArticle content (may be HTML or markdown)
data.content.htmlstringArticle content in HTML format (if available)
data.content.markdownstringArticle content in markdown format (if available)
data.seo_dataobjectSEO metadata including meta title, description, and keywords
data.meta_descriptionstringMeta description for the article
data.featured_imagestring | nullURL to the featured image (if set)
data.published_atstring | nullISO 8601 timestamp when the article was published
data.created_atstringISO 8601 timestamp when the article was created
data.updated_atstringISO 8601 timestamp when the article was last updated
data.authorobject | nullAuthor information (id and email)

Authentication

Bearer Token Authentication

When using Bearer token authentication, the webhook request includes an Authorization header:

Authorization: Bearer YOUR_TOKEN_HERE

Basic Authentication

When using Basic authentication, the webhook request includes an Authorization header with base64-encoded credentials:

Authorization: Basic base64(username:password)

HMAC Signature Verification

If you provide a secret key, webhook requests will include HMAC SHA-256 signatures in the headers:

  • X-Webhook-Signature: The HMAC SHA-256 signature of the request body
  • X-Webhook-Signature-Algorithm: Always sha256

To verify the signature on your server:

const crypto = require('crypto');

function verifyWebhookSignature(payload, signature, secretKey) {
  const expectedSignature = crypto
    .createHmac('sha256', secretKey)
    .update(payload)
    .digest('hex');
  
  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expectedSignature)
  );
}

// Example usage
const rawBody = JSON.stringify(webhookPayload);
const signature = request.headers['x-webhook-signature'];
const isValid = verifyWebhookSignature(rawBody, signature, YOUR_SECRET_KEY);

Webhook Headers

All webhook requests include the following headers:

HeaderDescription
Content-TypeAlways application/json
User-AgentEssenseo-Webhook/1.0
AuthorizationAuthentication header (if configured)
X-Webhook-SignatureHMAC SHA-256 signature (if secret key is configured)
X-Webhook-Signature-AlgorithmAlways sha256 (if secret key is configured)

Handling Webhook Requests

Best Practices

  1. Respond quickly: Your endpoint should respond within 10 seconds to avoid timeouts
  2. Return 2xx status codes: Return 200 OK or 201 Created to indicate successful processing
  3. Idempotency: Design your endpoint to handle duplicate webhook deliveries gracefully
  4. Verify signatures: Always verify HMAC signatures when a secret key is configured
  5. Log requests: Log incoming webhook requests for debugging and auditing

Example Webhook Handler (Node.js/Express)

const express = require('express');
const crypto = require('crypto');
const app = express();

app.use(express.json({ verify: (req, res, buf) => {
  req.rawBody = buf.toString('utf8');
}}));

app.post('/webhook', async (req, res) => {
  try {
    // Verify HMAC signature if secret key is configured
    const signature = req.headers['x-webhook-signature'];
    const secretKey = process.env.WEBHOOK_SECRET_KEY;
    
    if (secretKey && signature) {
      const expectedSignature = crypto
        .createHmac('sha256', secretKey)
        .update(req.rawBody)
        .digest('hex');
      
      if (signature !== expectedSignature) {
        return res.status(401).json({ error: 'Invalid signature' });
      }
    }

    // Verify Bearer token if using Bearer authentication
    const authHeader = req.headers['authorization'];
    if (authHeader && authHeader.startsWith('Bearer ')) {
      const token = authHeader.substring(7);
      if (token !== process.env.WEBHOOK_BEARER_TOKEN) {
        return res.status(401).json({ error: 'Invalid token' });
      }
    }

    const { event_type, data } = req.body;

    // Process the webhook based on event type
    switch (event_type) {
      case 'article.published':
        await handleArticlePublished(data);
        break;
      case 'article.updated':
        await handleArticleUpdated(data);
        break;
      case 'article.deleted':
        await handleArticleDeleted(data);
        break;
      default:
        console.log('Unknown event type:', event_type);
    }

    // Always return 200 OK to acknowledge receipt
    res.status(200).json({ received: true });
  } catch (error) {
    console.error('Webhook processing error:', error);
    // Return 500 to trigger retry
    res.status(500).json({ error: 'Internal server error' });
  }
});

async function handleArticlePublished(data) {
  console.log('Article published:', data.title);
  // Your logic here: sync to CMS, send notifications, etc.
}

async function handleArticleUpdated(data) {
  console.log('Article updated:', data.title);
  // Your logic here: update external systems, etc.
}

async function handleArticleDeleted(data) {
  console.log('Article deleted:', data.article_id);
  // Your logic here: remove from external systems, etc.
}

app.listen(3000, () => {
  console.log('Webhook server listening on port 3000');
});

Example Webhook Handler (Python/Flask)

from flask import Flask, request, jsonify
import hmac
import hashlib
import os

app = Flask(__name__)

def verify_signature(payload, signature, secret_key):
    """Verify HMAC SHA-256 signature"""
    expected_signature = hmac.new(
        secret_key.encode('utf-8'),
        payload.encode('utf-8'),
        hashlib.sha256
    ).hexdigest()
    
    return hmac.compare_digest(signature, expected_signature)

@app.route('/webhook', methods=['POST'])
def webhook():
    try:
        # Get raw body for signature verification
        raw_body = request.get_data(as_text=True)
        
        # Verify HMAC signature if secret key is configured
        signature = request.headers.get('X-Webhook-Signature')
        secret_key = os.environ.get('WEBHOOK_SECRET_KEY')
        
        if secret_key and signature:
            if not verify_signature(raw_body, signature, secret_key):
                return jsonify({'error': 'Invalid signature'}), 401
        
        # Verify Bearer token if using Bearer authentication
        auth_header = request.headers.get('Authorization')
        if auth_header and auth_header.startswith('Bearer '):
            token = auth_header[7:]
            if token != os.environ.get('WEBHOOK_BEARER_TOKEN'):
                return jsonify({'error': 'Invalid token'}), 401
        
        data = request.json
        event_type = data.get('event_type')
        article_data = data.get('data')
        
        # Process the webhook based on event type
        if event_type == 'article.published':
            handle_article_published(article_data)
        elif event_type == 'article.updated':
            handle_article_updated(article_data)
        elif event_type == 'article.deleted':
            handle_article_deleted(article_data)
        
        return jsonify({'received': True}), 200
    except Exception as e:
        print(f'Webhook processing error: {e}')
        return jsonify({'error': 'Internal server error'}), 500

def handle_article_published(data):
    print(f"Article published: {data.get('title')}")
    # Your logic here

def handle_article_updated(data):
    print(f"Article updated: {data.get('title')}")
    # Your logic here

def handle_article_deleted(data):
    print(f"Article deleted: {data.get('article_id')}")
    # Your logic here

if __name__ == '__main__':
    app.run(port=3000)

Retry Logic

The webhook system automatically retries failed deliveries:

  • Retry conditions: Only server errors (HTTP 5xx) trigger retries
  • Retry attempts: Up to 3 total attempts (1 initial + 2 retries)
  • Retry delays: Exponential backoff (1 second, then 2 seconds)
  • Client errors (4xx): Not retried (e.g., invalid authentication, bad request)

Webhook Delivery Logs

All webhook deliveries are logged and can be viewed in the webhook management interface. Logs include:

  • Event type
  • Delivery timestamp
  • HTTP status code
  • Response body
  • Error messages (if any)
  • Number of delivery attempts

Security Considerations

  1. Use HTTPS: Always use HTTPS URLs for webhook endpoints
  2. Verify signatures: Always verify HMAC signatures when a secret key is configured
  3. Use authentication: Prefer Bearer token or Basic authentication over no authentication
  4. Validate payloads: Validate the structure and content of incoming webhook payloads
  5. Rate limiting: Implement rate limiting on your webhook endpoint to prevent abuse
  6. IP whitelisting: Consider IP whitelisting if your infrastructure supports it

URL Validation

For security reasons, webhook URLs are validated to prevent SSRF (Server-Side Request Forgery) attacks:

  • ✅ Only HTTP and HTTPS protocols are allowed
  • ❌ Localhost and private IP ranges are blocked
  • ❌ Invalid URLs are rejected

Limitations

  • One webhook per site: Only one active webhook configuration is allowed per site
  • 10-second timeout: Webhook requests timeout after 10 seconds
  • No custom headers: You cannot add custom headers beyond authentication headers
  • No custom payload format: The payload structure is fixed and cannot be customized

Troubleshooting

Webhook Not Receiving Events

  1. Check webhook status: Ensure the webhook is marked as "Active"
  2. Verify event subscriptions: Confirm the webhook is subscribed to the correct events
  3. Check delivery logs: Review the webhook delivery logs for error messages
  4. Test webhook: Use the "Test Webhook" feature to verify the endpoint is reachable

Authentication Failures

  1. Bearer token: Verify the token matches exactly (check for extra spaces)
  2. Basic auth: Ensure username and password are correct
  3. HMAC signature: Verify the secret key matches and signature verification logic is correct

Timeout Errors

  1. Response time: Ensure your endpoint responds within 10 seconds
  2. Async processing: Process webhooks asynchronously and return immediately
  3. Optimize endpoint: Reduce processing time in your webhook handler

API Reference

Webhook Configuration Schema

interface WebhookConfiguration {
  id: string;
  site_id: string;
  name: string;
  url: string;
  events: ('article.published' | 'article.updated' | 'article.deleted')[];
  auth_method: 'none' | 'bearer' | 'basic';
  auth_token?: string | null;
  auth_username?: string | null;
  auth_password?: string | null;
  secret_key?: string | null;
  is_active: boolean;
  created_at: string;
  updated_at: string;
  created_by?: string | null;
}

Support

For issues or questions about webhook integration, please contact support or refer to the webhook delivery logs in your dashboard.