Overview
Custom webhooks allow you to receive article data from Publish Owl and handle it however you need. This is perfect for integrating with custom CMS platforms, databases, static site generators, or any other system that can receive HTTP requests.
When you configure a webhook, Publish Owl will send HTTP requests to your endpoint whenever articles are created, updated, or need to be fetched for refresh operations.
Step 1: Create Your Webhook Endpoint
First, you need to create an HTTP endpoint that can receive POST requests from Publish Owl. This endpoint must:
- Accept
POSTrequests with JSON body - Return a
200status code on success - Return a JSON response with specific fields (explained below)
- Be publicly accessible (Publish Owl's servers need to reach it)
Example: Node.js with Express
const express = require('express');
const app = express();
app.use(express.json());
app.post('/webhook/publish-owl', async (req, res) => {
const { action, postId, data } = req.body;
console.log('Received webhook:', action);
try {
switch (action) {
case 'ping':
// Test request - just acknowledge
return res.json({ success: true });
case 'create':
// Handle new article creation
const newArticle = await saveToDatabase({
title: data.title,
content: data.content,
slug: data.slug,
status: data.status,
publishDate: data.publishDate,
featuredImage: data.featuredImage,
});
// Return the post ID and URL so Publish Owl can track it
return res.json({
postId: newArticle.id,
url: `https://yoursite.com/blog/${newArticle.slug}`,
status: newArticle.status
});
case 'update':
// Handle article updates (e.g., from content refresh)
// Note: slug is not sent - we preserve original URLs for SEO
await updateInDatabase(postId, {
title: data.title, // May change for time-sensitive content
content: data.content,
status: data.status,
updatedAt: data.updatedAt, // For SEO freshness signals
featuredImage: data.featuredImage,
categories: data.categories,
tags: data.tags,
excerpt: data.excerpt,
});
return res.json({ success: true });
case 'fetch':
// Return existing article data (for content refresh comparison)
const article = await getFromDatabase(postId);
return res.json({
title: article.title,
content: article.content,
slug: article.slug,
status: article.status
});
default:
return res.status(400).json({ error: 'Unknown action' });
}
} catch (error) {
console.error('Webhook error:', error);
return res.status(500).json({ error: error.message });
}
});
app.listen(3000, () => {
console.log('Webhook server running on port 3000');
}); Example: Python with Flask
from flask import Flask, request, jsonify
app = Flask(__name__)
@app.route('/webhook/publish-owl', methods=['POST'])
def handle_webhook():
payload = request.json
action = payload.get('action')
post_id = payload.get('postId')
data = payload.get('data', {})
print(f"Received webhook: {action}")
try:
if action == 'ping':
return jsonify({'success': True})
elif action == 'create':
# Save to your database
new_article = save_to_database(
title=data.get('title'),
content=data.get('content'),
slug=data.get('slug'),
status=data.get('status'),
publish_date=data.get('publishDate'),
featured_image=data.get('featuredImage')
)
return jsonify({
'postId': str(new_article.id),
'url': f"https://yoursite.com/blog/{new_article.slug}",
'status': new_article.status
})
elif action == 'update':
# Note: slug is not sent - we preserve original URLs for SEO
update_in_database(
post_id,
title=data.get('title'), # May change for time-sensitive content
content=data.get('content'),
status=data.get('status'),
updated_at=data.get('updatedAt'), # For SEO freshness signals
featured_image=data.get('featuredImage'),
categories=data.get('categories'),
tags=data.get('tags'),
excerpt=data.get('excerpt')
)
return jsonify({'success': True})
elif action == 'fetch':
article = get_from_database(post_id)
return jsonify({
'title': article.title,
'content': article.content,
'slug': article.slug,
'status': article.status
})
else:
return jsonify({'error': 'Unknown action'}), 400
except Exception as e:
print(f"Webhook error: {e}")
return jsonify({'error': str(e)}), 500
if __name__ == '__main__':
app.run(port=3000) Step 2: Deploy Your Endpoint
Your webhook endpoint must be publicly accessible. Here are some options:
- Cloud Functions: AWS Lambda, Google Cloud Functions, Cloudflare Workers, Vercel Functions
- Hosting Platforms: Heroku, Railway, Render, DigitalOcean App Platform
- Your Own Server: Any server with a public IP and domain
- For Testing: Use ngrok to expose a local server temporarily
https://yoursite.com/blog/my-article)
must actually exist and render the article. For static site generators, this typically means creating a
dynamic route like /blog/[slug] that fetches and displays the article data.
Step 3: Configure in Publish Owl
Once your endpoint is deployed:
- Go to Sites from the top menu
- Click + Add Site
- Enter a name for your site (e.g., "My Custom Blog")
- Select Custom Webhook as the CMS type
- Enter your webhook URL (e.g.,
https://yoursite.com/webhook/publish-owl) - Choose the HTTP method (usually POST)
- (Optional) Add an authorization header if your endpoint requires authentication
- Click Save Site
Publish Owl will send a test "ping" request to verify your endpoint is reachable. If it returns a 200 status code, your site will be saved.
Payload Reference
Here's the complete structure of payloads Publish Owl sends to your webhook:
Create Action (New Article)
{
"action": "create",
"data": {
"title": "10 Best Coffee Shops in Seattle",
"content": "<h2>Introduction</h2><p>Seattle is known for...</p>",
"slug": "best-coffee-shops-seattle",
"status": "publish", // "draft", "publish", or "scheduled" (verb form)
"publishDate": "2026-01-15T09:00:00Z", // ISO 8601 (if scheduled)
"featuredImage": "https://storage.example.com/images/coffee.jpg",
"categories": [1, 5], // Category IDs if configured
"tags": ["coffee", "seattle"], // Tag names if configured
"excerpt": "Discover the best...",
"metadata": {} // Additional metadata
}
} Expected Response for Create
{
"postId": "abc123", // Required: Your system's ID for this article
"url": "https://yoursite.com/blog/best-coffee-shops-seattle", // Required: Live URL
"status": "published", // Optional: Confirmation of status
"metadata": {} // Optional: Any extra data to store
} Update Action (Content Refresh)
{
"action": "update",
"postId": "abc123", // The ID you returned when created
"data": {
"title": "10 Best Coffee Shops in Seattle (Updated 2026)",
"content": "<h2>Introduction</h2><p>Updated content...</p>",
"status": "publish",
"publishDate": "2026-01-15T09:00:00Z", // Original publish date
"updatedAt": "2026-06-20T14:30:00Z", // Last modified (for SEO)
"featuredImage": "https://storage.example.com/images/coffee-new.jpg",
"categories": [1, 5], // Category IDs if configured
"tags": ["coffee", "seattle"], // Tag names if configured
"excerpt": "Discover the best...",
"metadata": {}
}
} Note: The slug field is intentionally not sent on updates to prevent
breaking URLs, which is bad for SEO. Your endpoint should preserve the original slug.
Tip: The title field is included in updates, which is useful for
time-sensitive articles like "Best Laptops - January 2026" that need title updates during scheduled refreshes.
Fetch Action (Get Current Content)
// Request
{
"action": "fetch",
"postId": "abc123"
}
// Expected Response
{
"title": "10 Best Coffee Shops in Seattle",
"content": "<h2>Introduction</h2><p>Current content...</p>",
"slug": "best-coffee-shops-seattle",
"status": "published",
"metadata": {}
} Ping Action (Connection Test)
// Request (sent when saving site configuration)
{
"test": true,
"action": "ping"
}
// Expected Response (any 200 response works)
{
"success": true
} Authentication
To secure your webhook endpoint, you can configure an authorization header in Publish Owl. This header will be sent with every request.
Example: Bearer Token
In Publish Owl's site configuration, enter: Bearer your-secret-token-here
Then in your endpoint, verify it:
// Node.js/Express
app.post('/webhook/publish-owl', (req, res) => {
const authHeader = req.headers['authorization'];
if (authHeader !== 'Bearer your-secret-token-here') {
return res.status(401).json({ error: 'Unauthorized' });
}
// ... handle request
}); - Always use HTTPS in production
- Use a strong, randomly generated token
- Store your token as an environment variable, not in code
- Consider IP allowlisting if your platform supports it
Troubleshooting
"Webhook error: 404"
Your endpoint URL is incorrect or the endpoint doesn't exist. Double-check the URL and make sure your server is running and the route is properly configured.
"Webhook error: 401" or "403"
Authentication failed. Verify that the authorization header in Publish Owl matches exactly what your endpoint expects (including "Bearer " prefix if used).
"Webhook error: 500"
Your endpoint threw an error. Check your server logs for the specific error. Common causes: database connection issues, missing required fields, or code exceptions.
"Webhook error: ECONNREFUSED" or timeout
Publish Owl can't reach your endpoint. Make sure your server is running, publicly accessible, and not blocked by a firewall. If testing locally, use ngrok or similar.
Article created but page shows 404
The webhook succeeded but the URL doesn't exist. This usually means your frontend pages aren't
deployed or configured. Verify that your dynamic route (e.g., /blog/[slug]) exists
and can fetch/display the article data your webhook stored.
Article URL is incorrect
Make sure your endpoint returns the correct url field in the response.
This is what Publish Owl stores and displays as the article's live URL.
Content refresh not working
For content refresh to work, your endpoint must handle the fetch action and return
the current article content. If you don't need refresh support, you can return a 404 or empty response.
Summary
Custom webhooks give you complete control over how Publish Owl's articles are stored and served. The key points to remember:
- Handle
ping,create,update, andfetchactions - Return
postIdandurlwhen creating articles - Use HTTPS and authentication in production
- Return proper HTTP status codes (200 for success, 4xx/5xx for errors)
If you have questions or run into issues, check your server logs first - they usually reveal exactly what went wrong.