Custom Webhook Integration

Connect BlogSEO to any platform or custom application using webhooks. Receive article data via HTTP POST requests whenever articles are published.

Custom webhooks allow you to integrate BlogSEO with any system that can receive HTTP requests, including custom CMS platforms, static site generators, headless CMS systems, or your own backend services.

Setting Up Webhook Integration

Navigate to Settings → Integrations in your BlogSEO dashboard, click Custom Webhook, then click Create Webhook. You'll need to configure:

1. Webhook URL

Enter the endpoint URL where BlogSEO will send article data. This must be a publicly accessible HTTPS URL that accepts POST requests.

https://your-app.com/api/webhooks/blogseo

2. Content Format

Choose how you want to receive article content:

  • Markdown: Raw markdown content with formatting (recommended for further processing)
  • HTML: Pre-converted HTML ready for display

3. Authentication Method

Select how BlogSEO should authenticate requests to your endpoint:

Generated Shared Secret (Recommended)

BlogSEO generates a secure 64-character hex string that is sent with every request in the X-Webhook-Secret header. You'll need to store this secret and verify it on your server.

Custom Header

Use your own authentication header. Specify a custom header name and value (e.g., Authorization: Bearer your-api-key).

Webhook Payload Structure

When an article is published, BlogSEO sends a POST request with the following JSON payload:

{
    "article": {
        "id": "01234567-89ab-cdef-0123-456789abcdef",
        "slug": "your-article-title-as-url-slug",
        "title": "Your Article Title",
        "content": "# Article content in markdown or HTML...",
        "format": "markdown",
        "published_at": "2024-01-15T10:30:00.000Z",
        "main_image_url": "https://storage.blogseo.io/images/article-image.webp",
        "meta_description": "An SEO-optimized description for search engine results, under 160 characters.",
        "keyword": "target seo keyword"
    },
    "main_image": {
        "url": "https://storage.blogseo.io/images/article-image.webp",
        "alt": "Your Article Title"
    },
    "website": {
        "id": "fedcba98-7654-3210-fedc-ba9876543210",
        "baseUrl": "https://your-website.com"
    },
    "timestamp": "2024-01-15T10:30:00.000Z"
}

Payload Fields

FieldTypeDescription
article.idstring (UUID)Unique identifier for the article
article.slugstringURL-friendly slug generated from the title
article.titlestringThe article headline
article.contentstringArticle content in markdown or HTML format
article.formatstringEither "markdown" or "html"
article.published_atstringISO 8601 timestamp of publication
article.main_image_urlstringURL of the featured image
article.meta_descriptionstringSEO meta description for search engines (under 160 characters)
article.keywordstring | nullThe targeted SEO keyword, or null if none
main_image.urlstringSame as article.main_image_url
main_image.altstringAlt text for the image (same as title)
website.idstring (UUID)Unique identifier for the website
website.baseUrlstringBase URL of your website
timestampstringISO 8601 timestamp of when the webhook was sent

Slug

The slug field is an SEO-optimized, URL-friendly string automatically generated from the article title. Use it as the pathname for the page displaying the article content (e.g., /blog/your-article-slug). If you're storing articles in a database, consider indexing this field for better query performance.

Receiving Article Updates

When you republish an article to your custom webhook (for example, after editing the content in BlogSEO), your endpoint receives the same webhook request format as for new articles. The key to distinguishing between new articles and updates is the article.id field.

The article.id is a unique UUID that remains identical across all publications of the same article. This allows you to:

  • Deduplicate requests: Check if an article with this ID already exists in your database
  • Update existing content: Replace the content of the existing article instead of creating a duplicate
  • Track article history: Maintain version history by comparing the new content with the previous version

Implementation Example

Handling article updates

// Check if article already exists
const existingArticle = await db.article.findUnique({
    where: { blogseoId: article.id },
});
if (existingArticle) {
    // Update existing article
    await db.article.update({
        where: { blogseoId: article.id },
        data: {
            title: article.title,
            content: article.content,
            slug: article.slug,
            updatedAt: new Date(),
        },
    });
} else {
    // Create new article
    await db.article.create({
        data: {
            blogseoId: article.id,
            title: article.title,
            content: article.content,
            slug: article.slug,
        },
    });
}

Store the article.id (BlogSEO's UUID) in your database to enable proper deduplication. This is separate from your own internal article IDs.

Verifying Webhook Requests

Always verify that incoming webhook requests are authentic by checking the shared secret or custom header.

Verifying a BlogSEO webhook request

const signingSecret = request.headers['x-webhook-secret'];

if (signingSecret === process.env.BLOGSEO_WEBHOOK_SECRET) {
    // Process request
} else {
    throw new Error('Invalid signature');
}

Do not expose your webhook signing secret to the public. In particular, make sure your signing secret is not included in your git history and do not hardcode it in your application code. Use environment variables to store your signing secret securely.

Handling the Webhook Payload

After verifying the request, parse the JSON payload and process the article data.

Processing a BlogSEO webhook payload

// app/api/webhooks/blogseo/route.ts
import { NextRequest, NextResponse } from 'next/server';

export async function POST(request: NextRequest) {
    const signingSecret = request.headers.get('x-webhook-secret');
    if (signingSecret !== process.env.BLOGSEO_WEBHOOK_SECRET) {
        return NextResponse.json(
            { error: 'Invalid signature' },
            { status: 401 },
        );
    }
    const { article, website, timestamp } = await request.json();
    console.log(`Received article: ${article.title}`);
    console.log(`Slug: ${article.slug}`);
    console.log(`Format: ${article.format}`);
    console.log(`Image: ${article.main_image_url}`);
    // Save to your database, trigger builds, etc.
    // await saveArticle(article);
    return NextResponse.json({ success: true });
}

Testing Your Webhook

Use the Test button in the webhook configuration dialog to send a test payload to your endpoint. This allows you to verify:

  • Your endpoint is accessible
  • Authentication is working correctly
  • Your code correctly parses the payload

The test payload uses randomly generated UUIDs and sample content, allowing you to validate your integration without publishing a real article.

Response Requirements

Your webhook endpoint should:

  • Return a 2xx status code (200-299) to indicate success
  • Respond within 30 seconds
  • Return any status code outside 2xx to indicate failure

If your endpoint returns an error or times out, BlogSEO will retry the request automatically.

Use Cases

Custom webhooks are ideal for:

  • Static Site Generators: Trigger rebuilds when new content is published (Hugo, Jekyll, Gatsby, Next.js)
  • Headless CMS: Push content to Strapi, Sanity, or other headless platforms
  • Custom Applications: Integrate with your own backend services or databases
  • Notification Systems: Send alerts to Slack, Discord, or email when articles are published
  • Content Syndication: Automatically distribute content to multiple platforms

Rendering Articles in React

If you're building a React application and receiving content in markdown format, we recommend using the react-markdown package to render your articles.

ReactMarkdown follows the CommonMark specification by default, which does not include tables. To render tables and other GitHub Flavored Markdown (GFM) features like strikethrough and task lists, you need the remark-gfm plugin. For syntax highlighting in code blocks, use rehype-highlight.

Installation

npm install react-markdown remark-gfm rehype-highlight highlight.js

Basic Usage

Create a reusable article component that renders markdown content with full GFM support:

import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
import rehypeHighlight from 'rehype-highlight';
import 'highlight.js/styles/github.css';

interface ArticleProps {
    title: string;
    content: string;
    mainImageUrl?: string;
    mainImageAlt?: string;
}

export function Article({
    title,
    content,
    mainImageUrl,
    mainImageAlt,
}: ArticleProps) {
    return (
        <article>
            <h1>{title}</h1>
            {mainImageUrl && (
                <img src={mainImageUrl} alt={mainImageAlt || title} />
            )}
            <div className="markdown-content">
                <ReactMarkdown
                    remarkPlugins={[remarkGfm]}
                    rehypePlugins={[rehypeHighlight]}
                >
                    {content}
                </ReactMarkdown>
            </div>
        </article>
    );
}

Styling Tables

Add CSS styles for tables to display correctly. Here's an example you can include in your stylesheet:

.markdown-content table {
    width: 100%;
    border-collapse: collapse;
    margin: 1.5rem 0;
}

.markdown-content th,
.markdown-content td {
    border: 1px solid #d1d5db;
    padding: 0.75rem 1rem;
    text-align: left;
}

.markdown-content th {
    background-color: #f9fafb;
    font-weight: 600;
}

.markdown-content tr:nth-child(even) {
    background-color: #f9fafb;
}

You'll also want to add styles for headings, paragraphs, lists, code blocks, and other markdown elements for consistent rendering.

Troubleshooting

Webhook Not Receiving Requests

  • Verify your endpoint URL is correct and publicly accessible
  • Check that your server accepts POST requests at the specified path
  • Ensure your SSL certificate is valid (HTTPS is required)

Authentication Failures

  • Confirm you're checking the correct header (X-Webhook-Secret for generated secrets)
  • Verify the secret in your environment matches the one shown in BlogSEO
  • Check for trailing whitespace in your environment variables

Timeout Errors

  • Ensure your endpoint responds within 30 seconds
  • Process heavy operations asynchronously after sending the response
  • Consider using a queue system for time-consuming tasks

Payload Parsing Errors

  • Verify your endpoint expects application/json content type
  • Check that you're parsing the request body as JSON
  • Validate all expected fields exist before accessing them

Tables Not Rendering Correctly

If you're using the HTML format and tables appear unstyled, you'll need to add CSS styles for table elements. Here's example CSS you can use:

.article-content table {
    width: 100%;
    border-collapse: collapse;
    border: 1px solid #d1d5db;
    margin-bottom: 1rem;
}

.article-content th {
    background-color: #f9fafb;
    border: 1px solid #d1d5db;
    padding: 0.5rem 1rem;
    text-align: left;
    font-weight: 600;
}

.article-content td {
    border: 1px solid #d1d5db;
    padding: 0.5rem 1rem;
}

You may also want to style other HTML elements like headings, lists, blockquotes, and code blocks for consistent rendering.

Security Best Practices

  • Always verify the webhook signature before processing
  • Use HTTPS for your webhook endpoint
  • Store secrets in environment variables, never in code
  • Log webhook requests for debugging and auditing
  • Validate and sanitize all incoming data before use

Frequently Asked Questions

Can I have multiple webhook endpoints?
Currently, each BlogSEO website can have one webhook endpoint. Contact support if you need to send to multiple destinations. What happens if my endpoint is down?
BlogSEO automatically retries failed requests. If all attempts fail, the article status will be set to "preview" and you can manually retry from your dashboard. Can I change the webhook URL after creation?
Yes, you can update your webhook configuration at any time in the Integrations settings. Can I receive webhooks for article updates, not just new publications?
Currently, webhooks are sent when articles are published. Republishing an existing article will trigger a new webhook with the updated content. How do I detect if a webhook is for a new article or an update?
Use the article.id field to detect updates. This UUID remains identical when you republish an article. Store this ID in your database and check if it already exists when receiving a webhook. If it does, update the existing record instead of creating a new one. See the Receiving Article Updates section for implementation examples. My tables are showing as dashes and pipes instead of actual tables. How do I fix this?
This happens when using markdown format with a renderer that doesn't support GitHub Flavored Markdown (GFM) tables. If you're using react-markdown, you need to install and add the remark-gfm plugin. See the Rendering Articles in React section above for the full setup. Alternatively, you can switch to HTML format in your webhook settings, which sends pre-rendered tables that don't require additional plugins. For additional support, contact our team through the in-app support chat.

Was this page helpful?