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
- Go to your site's Settings page
- Navigate to the Integrations section
- Click on Webhooks
Step 2: Create a New Webhook
- Click the Create Webhook button
- 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 publishedarticle.updated- Triggered when an article is updatedarticle.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
| Field | Type | Description |
|---|---|---|
event_type | string | The type of event: article.published, article.updated, or article.deleted |
timestamp | string | ISO 8601 timestamp when the webhook was triggered |
data.article_id | string | Unique identifier for the article |
data.site_id | string | Unique identifier for the site |
data.title | string | Article title |
data.slug | string | URL-friendly slug for the article |
data.status | string | Current status of the article (e.g., "published", "draft") |
data.content.body | string | Article content (may be HTML or markdown) |
data.content.html | string | Article content in HTML format (if available) |
data.content.markdown | string | Article content in markdown format (if available) |
data.seo_data | object | SEO metadata including meta title, description, and keywords |
data.meta_description | string | Meta description for the article |
data.featured_image | string | null | URL to the featured image (if set) |
data.published_at | string | null | ISO 8601 timestamp when the article was published |
data.created_at | string | ISO 8601 timestamp when the article was created |
data.updated_at | string | ISO 8601 timestamp when the article was last updated |
data.author | object | null | Author 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 bodyX-Webhook-Signature-Algorithm: Alwayssha256
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:
| Header | Description |
|---|---|
Content-Type | Always application/json |
User-Agent | Essenseo-Webhook/1.0 |
Authorization | Authentication header (if configured) |
X-Webhook-Signature | HMAC SHA-256 signature (if secret key is configured) |
X-Webhook-Signature-Algorithm | Always sha256 (if secret key is configured) |
Handling Webhook Requests
Best Practices
- Respond quickly: Your endpoint should respond within 10 seconds to avoid timeouts
- Return 2xx status codes: Return
200 OKor201 Createdto indicate successful processing - Idempotency: Design your endpoint to handle duplicate webhook deliveries gracefully
- Verify signatures: Always verify HMAC signatures when a secret key is configured
- 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
- Use HTTPS: Always use HTTPS URLs for webhook endpoints
- Verify signatures: Always verify HMAC signatures when a secret key is configured
- Use authentication: Prefer Bearer token or Basic authentication over no authentication
- Validate payloads: Validate the structure and content of incoming webhook payloads
- Rate limiting: Implement rate limiting on your webhook endpoint to prevent abuse
- 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
- Check webhook status: Ensure the webhook is marked as "Active"
- Verify event subscriptions: Confirm the webhook is subscribed to the correct events
- Check delivery logs: Review the webhook delivery logs for error messages
- Test webhook: Use the "Test Webhook" feature to verify the endpoint is reachable
Authentication Failures
- Bearer token: Verify the token matches exactly (check for extra spaces)
- Basic auth: Ensure username and password are correct
- HMAC signature: Verify the secret key matches and signature verification logic is correct
Timeout Errors
- Response time: Ensure your endpoint responds within 10 seconds
- Async processing: Process webhooks asynchronously and return immediately
- 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.
