Custom Webhook Integration

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 POST requests with JSON body
  • Return a 200 status 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
Tip: Make sure to use HTTPS in production. Publish Owl will send sensitive article content to your endpoint, so encryption is important.
Don't forget frontend pages! Your webhook endpoint stores article data, but you also need frontend pages to display it. The URL you return (e.g., 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:

  1. Go to Sites from the top menu
  2. Click + Add Site
  3. Enter a name for your site (e.g., "My Custom Blog")
  4. Select Custom Webhook as the CMS type
  5. Enter your webhook URL (e.g., https://yoursite.com/webhook/publish-owl)
  6. Choose the HTTP method (usually POST)
  7. (Optional) Add an authorization header if your endpoint requires authentication
  8. 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
});
Security Best Practices:
  • 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, and fetch actions
  • Return postId and url when 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.

Was this helpful?