Skip to main content

Webhook Integration

Learn how to setup a Webhook to send new articles from IndexPilot to your custom website CMS. You can also use our webhook integration for N8N, Zapier, and Make.

Updated over 2 months ago

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

  • 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:

  1. Creates a hash of the payload using your secret

  2. Includes the signature in the X-IndexPilot-Signature header

  3. Includes a timestamp in the X-IndexPilot-Timestamp header

  4. 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:

  1. IndexPilot generates article → article.ready webhook fires

  2. Article is available in IndexPilot dashboard

  3. 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:

  1. Article syncs to CMS → article.published webhook fires

  2. Article is now live on your website

  3. 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:

  1. Fix the issue with your endpoint

  2. Go to Integrations → Webhook

  3. Toggle the integration back to active

  4. Test the connection

Did this answer your question?