Go to Integrations

Custom Webhook

Send finished articles to any platform that accepts HTTP requests — your own CMS, Zapier, Make, n8n, or a custom API your team builds.

Overview

1

When you publish an article via the Custom Webhook integration, SEOcraftAI sends a structured HTTP POST request to your configured endpoint.

2

The payload contains the article title, content (Markdown or HTML), keyword, SEO metadata, and a cover image URL.

!

Your endpoint must be publicly accessible over HTTPS. Localhost URLs will not work in production.

Setup

1

Go to Integrations -> Custom Webhook

2

Enter your endpoint URL — must start with https://

3

Select Auth Type: None, Bearer Token, or Custom Header. For Bearer Token paste your secret; for Custom Header enter the header name (e.g. X-API-Key) and value.

4

Click Send Test — SEOcraftAI POSTs a sample payload and shows the response status.

5

Click Save once the test succeeds.

i

The test payload has event set to "test" so your endpoint can distinguish test pings from real publishes.

Payload structure

Every publish sends this JSON body:json
{
  "event": "publish",
  "timestamp": "2026-01-01T00:00:00.000Z",
  "article": {
    "id": "f255c3e4-d3c9-4010-8998-462379beb0f3",
    "title": "Article Title",
    "slug": "article-title",
    "excerpt": "Short plain-text summary of the article…",
    "content_markdown": "# Article Title\n\nFull article in Markdown…",
    "content_html": "<h1>Article Title</h1><p>Full article in HTML…</p>",
    "format": "markdown",
    "images": ["https://storage.googleapis.com/seocraftai-image-bucket/covers/…"],
    "seo": {
      "meta_title": "Article Title",
      "meta_description": "First ~157 chars of article…",
      "keywords": ["target keyword"]
    },
    "published_at": "2026-01-01T00:00:00.000Z",
    "main_image_url": "https://storage.googleapis.com/seocraftai-image-bucket/covers/…"
  },
  "main_image": {
    "url": "https://storage.googleapis.com/seocraftai-image-bucket/covers/…",
    "alt": "Article Title"
  },
  "website": {
    "id": "site-uuid",
    "baseUrl": "https://yoursite.com"
  }
}

Payload fields

FieldTypeDescription
eventstring"publish" on real publishes, "test" on test pings
timestampISO 8601Time the webhook was dispatched
article.idUUIDStable identifier — store it to upsert on republish
article.titlestringArticle headline
article.slugstringSEO-friendly URL path component
article.excerptstringPlain-text summary (~160 chars), stripped of Markdown
article.content_markdownstringFull article body in Markdown
article.content_htmlstringFull article body converted to HTML
article.formatstring"markdown" or "html" — indicates which content field to use
article.imagesstring[]All image URLs found in the article (cover first, then inline)
article.seo.meta_titlestringSEO title (equals article title)
article.seo.meta_descriptionstringAuto-generated meta description (~157 chars)
article.seo.keywordsstring[]Target SEO keywords; empty array if none set
article.published_atISO 8601Publication timestamp
article.main_image_urlstring | nullCover image URL — null if no image generated yet
main_image.urlstring | nullSame as article.main_image_url, for convenience
main_image.altstring | nullAlt text for the image (equals article title)
website.idUUIDYour SEOcraftAI site ID
website.baseUrlstringYour site's base URL as configured in SEOcraftAI

Handling the cover image

1

Read article.main_image_url from the payload. If it is null, the article has no cover image yet — skip the image step.

2

Fetch the image with a standard HTTP GET (no auth needed). The response is image/jpeg.

3

Store the image in your own CDN, S3, or media library — do not rely on SEOcraftAI as permanent image hosting.

Node.js example:js
if (payload.article.main_image_url) {
  const res = await fetch(payload.article.main_image_url);
  const buffer = Buffer.from(await res.arrayBuffer());
  // upload buffer to S3, Cloudinary, WordPress media, etc.
  // then store the new URL in your CMS — don't link directly to SEOcraftAI
}
PHP example:php
if (!empty($payload['article']['main_image_url'])) {
  $img = file_get_contents($payload['article']['main_image_url']);
  // file_put_contents('/uploads/cover.jpg', $img);
}

Handling updates

1

The article.id UUID is stable across republications — it never changes for the same article.

2

Store the UUID in your database. On each incoming webhook, check if a record with that UUID already exists.

*

If found: update the existing record. If not found: insert a new one. This prevents duplicate posts when an article is edited and republished.

Upsert pattern (Node.js / SQL):js
const existing = await db.query(
  'SELECT id FROM posts WHERE SEOcraftAI_id = $1',
  [payload.article.id]
);
if (existing.rows.length > 0) {
  await db.query('UPDATE posts SET title=$1, content=$2 WHERE SEOcraftAI_id=$3',
    [payload.article.title, payload.article.content_markdown, payload.article.id]);
} else {
  await db.query('INSERT INTO posts (SEOcraftAI_id, title, content) VALUES ($1,$2,$3)',
    [payload.article.id, payload.article.title, payload.article.content_markdown]);
}

Response requirements

!

Your endpoint must return a 2xx status code (200-299) within 30 seconds. Any other response is treated as a failure.

1

Return the response immediately and process heavy work (image upload, rendering) asynchronously to stay within the time limit.

2

A minimal success response: HTTP 200 with any body, e.g. { "ok": true }.

Publishing articles

1

Open Integrations -> Custom Webhook from the sidebar.

2

Click Publish Article to expand the publish form.

3

Enter the article title and paste the Markdown content.

4

Click Send via Webhook — SEOcraftAI POSTs the full payload (title, content_markdown, content_html, SEO fields, cover image URL) to your endpoint.

5

The response status appears inline — green means your endpoint accepted it.

Common issues

1

Endpoint not reachable — verify your URL is publicly accessible over HTTPS with a valid SSL certificate. Test it with curl from another machine.

2

Auth failures — check for trailing whitespace in your token. Header names are case-sensitive on some servers.

3

Timeout errors — your endpoint must respond within 30 seconds. Move image processing, database writes, and rendering to a background job.

4

Image not showingmain_image_url points to SEOcraftAI's server. Fetch and re-host the image in your own storage — do not embed the URL directly in your CMS.

5

Duplicate posts — store article.id and use it to upsert instead of always inserting.

Verifying the request

1

Always verify incoming requests before processing. SEOcraftAI sends your secret in the Authorization header as a Bearer token or in a custom header you configure.

Next.js App Router — full receiver example:ts
// app/api/webhooks/blog/route.ts
import { NextRequest, NextResponse } from 'next/server';

export async function POST(request: NextRequest) {
  // 1. Verify the secret
  const authHeader = request.headers.get('authorization') ?? '';
  const secret = authHeader.startsWith('Bearer ')
    ? authHeader.slice(7).trim()
    : request.headers.get('x-webhook-secret') ?? '';

  if (secret !== process.env.SEOCRAFTAI_WEBHOOK_SECRET) {
    return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
  }

  // 2. Parse the payload
  const { event, article } = await request.json();

  // 3. Ignore test pings
  if (event === 'test') {
    return NextResponse.json({ ok: true });
  }

  // 4. Save to your database (upsert by article.id)
  await db.upsert({
    where: { seocraftaiId: article.id },
    create: {
      seocraftaiId: article.id,
      slug: article.slug,
      title: article.title,
      content: article.content_markdown,
      excerpt: article.excerpt,
      coverImageUrl: article.main_image_url,
      metaDescription: article.seo.meta_description,
    },
    update: {
      title: article.title,
      content: article.content_markdown,
      excerpt: article.excerpt,
      coverImageUrl: article.main_image_url,
    },
  });

  // 5. Return 2xx within 30 seconds
  return NextResponse.json({ ok: true });
}
!

Store SEOCRAFTAI_WEBHOOK_SECRET in your environment variables — never hardcode it.

Rendering markdown content

1

Use the react-markdown package to render article.content_markdown in React. Install it along with remark-gfm (tables, strikethrough) and rehype-highlight (code syntax highlighting):

bash
npm install react-markdown remark-gfm rehype-highlight highlight.js
Article component:tsx
import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
import rehypeHighlight from 'rehype-highlight';
import 'highlight.js/styles/github.css';

export function Article({ title, content, mainImageUrl }) {
  return (
    <article>
      <h1>{title}</h1>
      {mainImageUrl && <img src={mainImageUrl} alt={title} />}
      <div className="prose">
        <ReactMarkdown
          remarkPlugins={[remarkGfm]}
          rehypePlugins={[rehypeHighlight]}
        >
          {content}
        </ReactMarkdown>
      </div>
    </article>
  );
}
Add CSS so tables render correctly:css
.prose table { width: 100%; border-collapse: collapse; margin: 1.5rem 0; }
.prose th, .prose td { border: 1px solid #d1d5db; padding: 0.75rem 1rem; text-align: left; }
.prose th { background: #f9fafb; font-weight: 600; }
.prose tr:nth-child(even) { background: #f9fafb; }
!

Without remark-gfm, tables in the article will render as raw pipes and dashes instead of a formatted table.

2

If you chose HTML format instead, set article.format === 'html' and render with dangerouslySetInnerHTML={{ __html: article.content_html }} — tables are already pre-rendered.

Frequently asked questions

1

What happens if my endpoint is down? — SEOcraftAI automatically retries failed requests. If failures persist, the article stays in a pending state and you can manually retry from the Integrations dashboard.

2

Can I change the webhook URL after setup? — Yes. Update it anytime in Integrations → Custom Webhook. The next publish will use the new URL.

3

How do I tell new articles from updates? — Use article.id. Store it alongside your internal record and check for it on every incoming webhook. If it exists, update — if not, insert.

4

Can I have multiple webhook endpoints? — One endpoint per site is supported. Contact support if you need to fan out to multiple destinations.

5

Why are tables showing as pipes and dashes? — Your markdown renderer doesn't support GitHub Flavored Markdown (GFM). Install remark-gfm for react-markdown, or switch to HTML format in your SEOcraftAI webhook settings.

6

Do I need to re-host the cover image? — Yes. main_image_url points to SEOcraftAI's CDN. Fetch and store it in your own storage (S3, Cloudinary, etc.) — do not embed the URL directly in your CMS long-term.

Notes

Webhook credentials are saved with your account — you only need to configure them once.

One webhook endpoint per site is supported. Contact support if you need multiple endpoints.