Webhooks allow IndexPilot to automatically send your generated articles to any custom platform or service in real time. Think of it as a notification system that delivers article data to your server whenever an article is created, published, or updated.
When Should You Use Webhooks?
Custom CMS Integration: Connect to platforms not natively supported (Ghost, Medium, Strapi, etc.)
Workflow Automation: Trigger custom workflows when articles are ready (N8N, Zapier, Make)
Multi-Platform Publishing: Send articles to multiple destinations simultaneously
Data Processing: Process article data through your own systems
Notifications: Alert your team when new content is available
Analytics: Track article generation metrics in your own systems
Webhook Security: Signed vs Unsigned Requests
IndexPilot webhooks support both signed and unsigned requests:
Signed Requests (Recommended):
- Includes `X-IndexPilot-Signature` and `X-IndexPilot-Timestamp` headers
- Uses HMAC-SHA256 encryption with your webhook secret
- Prevents spoofing and replay attacks
Unsigned Requests
- No signature or timestamp headers
- Faster to implement and test
Quick Start Guide
Step 1: Create Your Webhook Endpoint
You’ll need a publicly accessible HTTPS endpoint that can receive POST requests from IndexPilot.
Example Node.js/Express Endpoint:
const express = require('express');
const crypto = require('crypto');
const app = express();
app.use(express.json());
app.post('/webhooks/indexpilot', (req, res) => {
const signature = req.headers['x-indexpilot-signature'];
const timestamp = req.headers['x-indexpilot-timestamp'];
const webhookSecret = process.env.INDEXPILOT_WEBHOOK_SECRET;
// Verify the webhook (see verification guide below)
if (!verifySignature(req.body, signature, webhookSecret, timestamp)) {
return res.status(401).json({ error: 'Invalid signature' });
}
// Process the article
const { event, article, site, event_id } = req.body;
console.log('Event ID:', event_id); // Use for idempotency
console.log('Event type:', event);
console.log('New article:', article.title);
console.log('Site:', site.name);
// Add your custom logic here:
// - Save to your database
// - Publish to your CMS
// - Send notifications
// - Trigger workflows
res.status(200).json({ success: true });
});
app.listen(3000);
Key Requirements:
✅ Must use HTTPS
✅ Must be publicly accessible
✅ Must respond within 30 seconds
✅ Must return a 2xx status code for success
✅ Should verify webhook signatures for security
✅ Should implement idempotency using event_id
Step 2: Configure Your Webhook in IndexPilot
Navigate to Integrations
Open your IndexPilot dashboard
Click Integrations in the sidebar
Select Webhook
Enter Your Webhook URL
Enter your endpoint URL (e.g., https://yourdomain.com/webhooks/indexpilot)
Click Test Connection to verify it's reachable
The test will send a sample payload to your endpoint
Configure Secret Management
You have two options for webhook secrets:
Option A: Managed Secret (Recommended)
Toggle off the custom secret option
IndexPilot will generate and manage a secure secret for you
IMPORTANT: Copy your webhook secret immediately after saving—it's only shown once!
Option B: Custom Secret
Toggle on the custom secret option
Enter your own secret key (minimum 32 characters recommended)
You'll use this secret for signature verification
Configure Event Triggers (Settings Tab)
Choose which events trigger webhooks:
article.ready – Article generation completed
Fires when AI finishes generating the article
Article is ready for review but not yet synced to CMS
article.published – Article published to CMS (Recommended)
Fires when article is successfully synced to your CMS
Means the article is now live on your site
Adjust Delivery Settings (Settings Tab)
Sign Requests: Keep enabled for security (includes signature headers)
Verify SSL: Keep enabled for security (disable only for local testing)
Max Retries: 3 (recommended) – automatic retries on failure
Timeout: 30 seconds (recommended) – wait time for your server
Backoff Multiplier: 2 (exponential backoff between retries)
Save Configuration
Click Save Configuration
Store your webhook secret securely (you won't see it again)
Step 3: Verify Webhook Signatures ⚠️ Critical!
Never trust incoming webhooks without signature verification. This ensures the request is from IndexPilot and prevents spoofed requests.
How Signature Verification Works
IndexPilot signs each webhook request with HMAC-SHA256:
Creates a hash of the payload using your secret
Includes the signature in the X-IndexPilot-Signature header
Includes a timestamp in the X-IndexPilot-Timestamp header
Your server verifies the signature matches
Node.js Verification Example:
const crypto = require('crypto');
function verifySignature(payload, signature, secret, timestamp) {
// Step 1: Check timestamp (prevent replay attacks)
const now = Date.now();
const requestTime = parseInt(timestamp, 10);
const fiveMinutes = 5 * 60 * 1000;
if (Math.abs(now - requestTime) > fiveMinutes) {
console.error('Webhook timestamp too old or in future');
return false;
}
// Step 2: Compute expected signature
const payloadString = JSON.stringify(payload);
const expectedSignature = crypto
.createHmac('sha256', secret)
.update(payloadString)
.digest('hex');
// Step 3: Remove 'sha256=' prefix if present
const providedSignature = signature.replace('sha256=', '');
// Step 4: Timing-safe comparison (prevents timing attacks)
try {
return crypto.timingSafeEqual(
Buffer.from(expectedSignature, 'hex'),
Buffer.from(providedSignature, 'hex')
);
} catch (e) {
return false;
}
}
Python Verification Example:
import hmac
import hashlib
import time
import json
def verify_signature(payload, signature, secret, timestamp):
# Check timestamp (within 5 minutes)
now = int(time.time() * 1000)
request_time = int(timestamp)
five_minutes = 5 * 60 * 1000
if abs(now - request_time) > five_minutes:
return False
# Compute expected signature
payload_string = json.dumps(payload, separators=(',', ':'))
expected_signature = hmac.new(
secret.encode('utf-8'),
payload_string.encode('utf-8'),
hashlib.sha256
).hexdigest()
# Remove prefix and compare
provided_signature = signature.replace('sha256=', '')
return hmac.compare_digest(expected_signature, provided_signature)
PHP Verification Example:
function verifySignature($payload, $signature, $secret, $timestamp) {
// Check timestamp
$now = round(microtime(true) * 1000);
$requestTime = intval($timestamp);
$fiveMinutes = 5 * 60 * 1000;
if (abs($now - $requestTime) > $fiveMinutes) {
return false;
}
// Compute expected signature
$payloadString = json_encode($payload);
$expectedSignature = hash_hmac('sha256', $payloadString, $secret);
// Remove prefix
$providedSignature = str_replace('sha256=', '', $signature);
// Timing-safe comparison
return hash_equals($expectedSignature, $providedSignature);
}
Webhook Payload Structure
Every webhook request includes structured JSON data:
{
"event_id": "a1b2c3d4e5f6...",
"event": "article.ready",
"timestamp": "2025-09-30T12:00:00Z",
"site": {
"id": "site123",
"name": "My Site",
"host": "https://example.com"
},
"article": {
"id": "abc123",
"slug": "example-article-how-to-build-better-content",
"title": "Example Article: How to Build Better Content",
"content": "<h1>Example Article...</h1><p>Full HTML content...</p>",
"summary": "Learn how to build better content...",
"seo_title": "Example Article: How to Build Better Content",
"seo_meta_description": "Learn how to build better content...",
"target_keyword": "build better content",
"main_image_url": "https://example.com/images/example.jpg",
"thumbnail_image_url": "https://example.com/images/example-thumb.jpg",
"article_type": "explainer",
"category": "Content Marketing",
"author_name": "IndexPilot AI",
"read_time_minutes": 5,
"is_featured": false,
"published_at": "2025-09-30T12:30:00Z",
"created_at": "2025-09-30T12:00:00Z",
"updated_at": "2025-09-30T12:00:00Z"
}
}
Field Descriptions
Field | Type | Description |
event_id | string | Unique identifier for this webhook event (use for idempotency) |
event | string | Event type: article.ready or article.published |
timestamp | string | ISO 8601 timestamp when the event occurred |
site.id | string | Your site's unique identifier in IndexPilot |
site.name | string | Your site's name |
site.host | string | Your site's domain |
article.id | string | Unique article identifier |
article.slug | string | URL-friendly slug for the article |
article.title | string | Article title |
article.content | string | Full HTML content of the article |
article.summary | string | Brief summary of the article |
article.seo_title | string | SEO-optimized title (50-60 characters) |
article.seo_meta_description | string | SEO meta description (150-160 characters) |
article.target_keyword | string | Primary keyword the article targets |
article.main_image_url | string | URL to the main article image |
article.thumbnail_image_url | string | URL to the thumbnail image |
article.article_type | string | Type: explainer, listicle, step_by_step_guide, comparison |
article.category | string | Article category |
article.author_name | string | Author name |
article.read_time_minutes | number | Estimated read time in minutes |
article.is_featured | boolean | Whether the article is featured |
article.published_at | string | When article was published (null if not yet published) |
article.created_at | string | When article was created |
article.updated_at | string | When article was last updated |
Webhook Headers
Each request contains important headers:
Header | Description | Example |
X-IndexPilot-Signature | HMAC-SHA256 signature | sha256=abc123... |
X-IndexPilot-Timestamp | Unix timestamp (milliseconds) | 1696089600000 |
Content-Type | Always application/json | application/json |
User-Agent | IndexPilot webhook identifier | IndexPilot-Webhook/1.0 |
X-IndexPilot-Version | API version | 1.0 |
Event Types
We recommend using article.published event type only if you're reviewing and making edits to articles.
article.ready
Fires when AI finishes generating the article (before CMS sync).
Use this when you want to:
Review articles before publishing
Run custom validation
Add manual approval steps
Process article data in your system
Typical flow:
IndexPilot generates article → article.ready webhook fires
Article is available in IndexPilot dashboard
Article has NOT been synced to your CMS yet
article.published
Fires when the article is successfully synced to your CMS (Webflow, WordPress, Shopify, Framer, or Webhook).
Use this when you want to:
Notify your team that content is live
Update external systems
Trigger marketing automation
Track publishing metrics
Typical flow:
Article syncs to CMS → article.published webhook fires
Article is now live on your website
article.published_at timestamp is populated
Implement Idempotency Using event_id
Webhooks may be delivered more than once due to retries. Use the event_id field to prevent duplicate processing:
const processedEvents = new Set(); // In production, use a database
app.post('/webhooks/indexpilot', (req, res) => {
const { event_id } = req.body;
// Check if already processed
if (processedEvents.has(event_id)) {
console.log('Duplicate event, skipping');
return res.status(200).json({ success: true });
}
// Process the webhook
processArticle(req.body);
// Mark as processed
processedEvents.add(event_id);
res.status(200).json({ success: true });
});
Troubleshooting
Problem: Webhooks Not Received
Solutions:
Verify your endpoint is publicly accessible via HTTPS
Check firewall rules allow incoming requests from IndexPilot
Ensure you're returning a 2xx status code
Check the Delivery Logs tab for error details
Verify your endpoint doesn't timeout (must respond within 30 seconds)
Problem: Signature Verification Failed
Solutions:
Verify you're using the correct webhook secret
Check timestamp validation (must be within 5 minutes)
Ensure you're hashing the raw request body (not parsed JSON)
Use timing-safe comparison for security
Remove the sha256= prefix before comparison
Problem: Duplicate Webhooks
Solutions:
Implement idempotency using the event_id field
Store processed event IDs in a database
Return success even for duplicate events
Problem: Webhook Integration Paused
Why it happens:
IndexPilot automatically pauses integrations after 10 consecutive failures to protect your endpoint.
Solutions:
Fix the issue with your endpoint
Go to Integrations → Webhook
Toggle the integration back to active
Test the connection
