Webhook - Setting up your API/Webhook Integration

Written By Founder@SEOProAI

Last updated 6 months ago

JavaScript Webhook Integration Guide

This guide will help you integrate SEOPro AI's webhook system with any JavaScript application (Node.js, Express, Next.js, React, etc.) to automatically receive blog post notifications.

Overview

When SEOPro AI publishes a blog post, it will send a webhook notification to your application. Your app will receive the blog data and can automatically process or store the articles as needed.

Setting Up Your Webhook

To set up a webhook integration, you'll need to provide the following information:

  • Integration Name: A unique name for your webhook integration (2-30 characters, no special characters)

  • Webhook Endpoint: The URL where you want to receive webhook events (must be a valid HTTPS URL)

  • Access Token: A secret key used to verify the authenticity of incoming webhook requests

Implementing Webhook Authentication

To ensure the security of your webhook, we use a Bearer token to authenticate incoming requests. Here's how to verify incoming webhooks:

Example in Node.js/Express:

const express = require('express');
const app = express();

// This should be securely stored in environment variables
const ACCESS_TOKEN = process.env.SEOPRO_WEBHOOK_TOKEN;

function validateAccessToken(req) {
  const authHeader = req.headers.authorization;
  if (!authHeader || !authHeader.startsWith("Bearer ")) {
    return false;
  }
  const token = authHeader.split(" ")[1];
  return token === ACCESS_TOKEN;
}

app.use(express.json());

app.post('/webhook', (req, res) => {
  // Validate access token
  if (!validateAccessToken(req)) {
    return res.status(401).json({ error: 'Invalid access token' });
  }

  // Validate event type
  if (req.body.event_type !== 'publish_articles') {
    return res.status(400).json({ error: 'Unsupported event type' });
  }

  // Process the webhook payload
  const { articles } = req.body.data;
  
  articles.forEach(article => {
    console.log(`Received article: ${article.title}`);
    // Process each article (save to database, file system, etc.)
    processArticle(article);
  });

  res.status(200).json({ message: 'Webhook processed successfully' });
});

async function processArticle(article) {
  // Implement your article processing logic here
  // Examples:
  // - Save to database
  // - Write to file system
  // - Send to CMS
  // - Trigger other workflows
  
  console.log('Processing article:', {
    id: article.id,
    title: article.title,
    slug: article.slug
  });
}

app.listen(3000, () => {
  console.log('Webhook server listening on port 3000');
});

Example in Next.js API Routes:

// pages/api/webhook.js (Pages Router)
// or app/api/webhook/route.js (App Router)

export default function handler(req, res) {
  if (req.method !== 'POST') {
    return res.status(405).json({ error: 'Method not allowed' });
  }

  // Validate access token
  const authHeader = req.headers.authorization;
  if (!authHeader || !authHeader.startsWith('Bearer ')) {
    return res.status(401).json({ error: 'Missing authorization header' });
  }

  const token = authHeader.replace('Bearer ', '');
  if (token !== process.env.SEOPRO_WEBHOOK_TOKEN) {
    return res.status(401).json({ error: 'Invalid access token' });
  }

  // Validate event type
  if (req.body.event_type !== 'publish_articles') {
    return res.status(400).json({ error: 'Unsupported event type' });
  }

  // Process articles
  const { articles } = req.body.data;
  
  articles.forEach(article => {
    // Process each article
    processArticle(article);
  });

  res.status(200).json({ message: 'Webhook processed successfully' });
}

function processArticle(article) {
  // Your processing logic here
  console.log(`Processing: ${article.title}`);
}

Example with Fastify:

const fastify = require('fastify')({ logger: true });

const ACCESS_TOKEN = process.env.SEOPRO_WEBHOOK_TOKEN;

// Register JSON parser
fastify.register(require('@fastify/formbody'));

fastify.post('/webhook', async (request, reply) => {
  // Validate access token
  const authHeader = request.headers.authorization;
  if (!authHeader || !authHeader.startsWith('Bearer ')) {
    return reply.status(401).send({ error: 'Invalid authorization header' });
  }

  const token = authHeader.replace('Bearer ', '');
  if (token !== ACCESS_TOKEN) {
    return reply.status(401).send({ error: 'Invalid access token' });
  }

  // Process webhook
  const { event_type, data } = request.body;
  
  if (event_type !== 'publish_articles') {
    return reply.status(400).send({ error: 'Unsupported event type' });
  }

  // Process articles
  for (const article of data.articles) {
    await processArticle(article);
  }

  return { message: 'Webhook processed successfully' };
});

async function processArticle(article) {
  // Your processing logic
  console.log(`Processing: ${article.title}`);
}

const start = async () => {
  try {
    await fastify.listen({ port: 3000 });
  } catch (err) {
    fastify.log.error(err);
    process.exit(1);
  }
};
start();

Publish Articles Event

When an article is published, we'll send a POST request to your webhook endpoint with a JSON payload. The payload will include:

  • event_type: The type of event that occurred (always "publish_articles" in this case)

  • timestamp: The time the event occurred (ISO 8601 format)

  • data: An object containing event-specific data, including an array of affected articles

Article Data Properties

Each article in the payload will contain the following properties:

  • id: Unique identifier for the article

  • title: The title of the article

  • content_markdown: The article content in Markdown format

  • content_html: The article content in HTML format

  • meta_description: A brief description of the article for SEO purposes

  • created_at: The timestamp when the article was created (ISO 8601 format)

  • image_url: The URL of the main image associated with the article

  • slug: The URL-friendly version of the article title used for permalinks

  • tags: An array of tags associated with the article

Example Payload

{
  "event_type": "publish_articles",
  "timestamp": "2023-04-01T12:00:00Z",
  "data": {
    "articles": [
      {
        "id": "123456",
        "title": "How to Implement Webhooks",
        "content_markdown": "Webhooks are a powerful...",
        "content_html": "<p>Webhooks are a powerful...</p>",
        "meta_description": "Learn how to implement webhooks in your application",
        "created_at": "2023-03-31T10:30:00Z",
        "image_url": "https://example.com/images/webhook-article.jpg",
        "slug": "how-to-implement-webhooks",
        "tags": ["webhooks", "integration", "api"]
      },
      {
        "id": "789012",
        "title": "Best Practices for API Design",
        "content_markdown": "When designing an API...",
        "content_html": "<p>When designing an API...</p>",
        "meta_description": "Discover the best practices for designing robust APIs",
        "created_at": "2023-03-31T11:45:00Z",
        "image_url": "https://example.com/images/api-design-article.jpg",
        "slug": "best-practices-for-api-design",
        "tags": ["api", "design", "best practices"]
      }
    ]
  }
}

Article Processing Examples

Save to Database (MongoDB)

const { MongoClient } = require('mongodb');

async function processArticle(article) {
  const client = new MongoClient(process.env.MONGODB_URI);
  
  try {
    await client.connect();
    const db = client.db('blog');
    const collection = db.collection('articles');
    
    // Upsert article (update if exists, insert if new)
    await collection.replaceOne(
      { seoProId: article.id },
      {
        seoProId: article.id,
        title: article.title,
        content: article.content_html,
        contentMarkdown: article.content_markdown,
        metaDescription: article.meta_description,
        slug: article.slug,
        featuredImage: article.image_url,
        tags: article.tags,
        publishedAt: new Date(article.created_at),
        createdAt: new Date()
      },
      { upsert: true }
    );
    
    console.log(`Article saved: ${article.title}`);
  } finally {
    await client.close();
  }
}

Save to File System (Markdown)

const fs = require('fs').promises;
const path = require('path');

async function processArticle(article) {
  const frontmatter = `---
title: "${article.title.replace(/"/g, '\\"')}"
description: "${article.meta_description?.replace(/"/g, '\\"') || ''}"
publishedAt: "${article.created_at}"
image: "${article.image_url || ''}"
tags: [${article.tags.map(tag => `"${tag}"`).join(', ')}]
seoProId: "${article.id}"
---

`;
  
  const content = frontmatter + article.content_markdown;
  const blogDir = path.join(process.cwd(), 'content/blog');
  const filePath = path.join(blogDir, `${article.slug}.md`);
  
  // Ensure directory exists
  await fs.mkdir(blogDir, { recursive: true });
  
  // Write the file
  await fs.writeFile(filePath, content, 'utf8');
  
  console.log(`Article saved to: ${filePath}`);
}

Send to WordPress REST API

const axios = require('axios');

async function processArticle(article) {
  const wordpressUrl = process.env.WORDPRESS_URL;
  const username = process.env.WORDPRESS_USERNAME;
  const password = process.env.WORDPRESS_APP_PASSWORD;
  
  const auth = Buffer.from(`${username}:${password}`).toString('base64');
  
  try {
    const response = await axios.post(`${wordpressUrl}/wp-json/wp/v2/posts`, {
      title: article.title,
      content: article.content_html,
      excerpt: article.meta_description,
      slug: article.slug,
      status: 'publish',
      meta: {
        seopro_id: article.id
      }
    }, {
      headers: {
        'Authorization': `Basic ${auth}`,
        'Content-Type': 'application/json'
      }
    });
    
    console.log(`Article published to WordPress: ${response.data.link}`);
  } catch (error) {
    console.error('Error publishing to WordPress:', error.message);
  }
}

Environment Variables

Create a .env file in your project root:

# Webhook Configuration
SEOPRO_WEBHOOK_TOKEN=your_generated_access_token_here

# Database (if using MongoDB)
MONGODB_URI=mongodb://localhost:27017/blog

# WordPress (if using WordPress integration)
WORDPRESS_URL=https://yoursite.com
WORDPRESS_USERNAME=your_username
WORDPRESS_APP_PASSWORD=your_app_password

Testing Your Webhook

1. Local Testing with ngrok

For local development, use ngrok to expose your local server:

# Install ngrok
npm install -g ngrok

# Start your webhook server
node webhook-server.js

# In another terminal, expose port 3000
ngrok http 3000

Use the ngrok HTTPS URL as your webhook endpoint in SEOPro AI.

2. Test Script

Create a test script to verify your webhook endpoint:

// test-webhook.js
const axios = require('axios');

const testPayload = {
  event_type: "publish_articles",
  timestamp: new Date().toISOString(),
  data: {
    articles: [
      {
        id: "test-123",
        title: "Test Article from SEOPro AI",
        content_markdown: "# Test Article\n\nThis is a test article.",
        content_html: "<h1>Test Article</h1><p>This is a test article.</p>",
        meta_description: "Test article description",
        created_at: new Date().toISOString(),
        image_url: "https://example.com/test-image.jpg",
        slug: "test-article-seopro-ai",
        tags: ["test", "webhook"]
      }
    ]
  }
};

async function testWebhook() {
  try {
    const response = await axios.post('http://localhost:3000/webhook', testPayload, {
      headers: {
        'Content-Type': 'application/json',
        'Authorization': `Bearer ${process.env.SEOPRO_WEBHOOK_TOKEN}`
      }
    });
    
    console.log('βœ… Webhook test successful:', response.data);
  } catch (error) {
    console.error('❌ Webhook test failed:', error.response?.data || error.message);
  }
}

testWebhook();

3. Production Testing

Once you've set up your webhook integration in SEOPro AI:

  1. Navigate to any article page in your dashboard

  2. Click the "Publish" button to publish the article

  3. Your webhook endpoint should receive a POST request with the article data

  4. Check your server logs to verify the webhook was received and processed correctly

Security Best Practices

  1. Always use HTTPS for your webhook endpoint in production

  2. Validate the Bearer token on every request

  3. Store your access token securely in environment variables

  4. Implement rate limiting to prevent abuse

  5. Log webhook events for debugging and monitoring

  6. Validate payload structure before processing

Error Handling

app.post('/webhook', async (req, res) => {
  try {
    // Validation logic here...
    
    const { articles } = req.body.data;
    const results = [];
    
    for (const article of articles) {
      try {
        await processArticle(article);
        results.push({ id: article.id, status: 'success' });
      } catch (error) {
        console.error(`Failed to process article ${article.id}:`, error);
        results.push({ id: article.id, status: 'error', error: error.message });
      }
    }
    
    res.status(200).json({
      message: 'Webhook processed',
      results
    });
    
  } catch (error) {
    console.error('Webhook processing error:', error);
    res.status(500).json({ error: 'Internal server error' });
  }
});

Troubleshooting

Common Issues

  1. 401 Unauthorized: Check your access token in both SEOPro AI dashboard and environment variables

  2. Webhook not received: Verify your endpoint URL is correct and publicly accessible

  3. SSL/TLS errors: Ensure your webhook endpoint uses valid HTTPS certificate

  4. Timeout errors: Make sure your webhook responds within 30 seconds

Debug Logging

Add comprehensive logging to help with troubleshooting:

app.post('/webhook', (req, res) => {
  console.log('Webhook received:', {
    timestamp: new Date().toISOString(),
    headers: req.headers,
    body: req.body,
    ip: req.ip
  });
  
  // Your processing logic...
});

This guide provides a solid foundation for integrating SEOPro AI webhooks with any JavaScript application. Choose the framework-specific example that best matches your setup and customize the article processing logic to fit your needs.