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
When you publish an article via the Custom Webhook integration, SEOcraftAI sends a structured HTTP POST request to your configured endpoint.
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
Go to Integrations -> Custom Webhook
Enter your endpoint URL — must start with https://
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.
Click Send Test — SEOcraftAI POSTs a sample payload and shows the response status.
Click Save once the test succeeds.
The test payload has event set to "test" so your endpoint can distinguish test pings from real publishes.
Payload structure
{
"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>",
"images": ["https://app.SEOcraftAI.com/api/articles/{id}/cover"],
"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://app.SEOcraftAI.com/api/articles/{id}/cover"
},
"main_image": {
"url": "https://app.SEOcraftAI.com/api/articles/{id}/cover",
"alt": "Article Title"
},
"website": {
"id": "site-uuid",
"baseUrl": "https://yoursite.com"
}
}Payload fields
| Field | Type | Description |
|---|---|---|
event | string | "publish" on real publishes, "test" on test pings |
timestamp | ISO 8601 | Time the webhook was dispatched |
article.id | UUID | Stable identifier — store it to upsert on republish |
article.title | string | Article headline |
article.slug | string | SEO-friendly URL path component |
article.excerpt | string | Plain-text summary (~160 chars), stripped of Markdown |
article.content_markdown | string | Full article body in Markdown |
article.content_html | string | Full article body converted to HTML |
article.images | string[] | All image URLs found in the article (cover first, then inline) |
article.seo.meta_title | string | SEO title (equals article title) |
article.seo.meta_description | string | Auto-generated meta description (~157 chars) |
article.seo.keywords | string[] | Target SEO keywords; empty array if none set |
article.published_at | ISO 8601 | Publication timestamp |
article.main_image_url | string | null | Cover image URL — null if no image generated yet |
main_image.url | string | null | Same as article.main_image_url, for convenience |
main_image.alt | string | null | Alt text for the image (equals article title) |
website.id | UUID | Your SEOcraftAI site ID |
website.baseUrl | string | Your site's base URL as configured in SEOcraftAI |
Handling the cover image
Read article.main_image_url from the payload. If it is null, the article has no cover image yet — skip the image step.
Fetch the image with a standard HTTP GET (no auth needed). The response is image/jpeg.
Store the image in your own CDN, S3, or media library — do not rely on SEOcraftAI as permanent image hosting.
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
}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
The article.id UUID is stable across republications — it never changes for the same article.
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.
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.
Return the response immediately and process heavy work (image upload, rendering) asynchronously to stay within the time limit.
A minimal success response: HTTP 200 with any body, e.g. { "ok": true }.
Publishing articles
Open Integrations -> Custom Webhook from the sidebar.
Click Publish Article to expand the publish form.
Enter the article title and paste the Markdown content.
Click Send via Webhook — SEOcraftAI POSTs the full payload (title, content_markdown, content_html, SEO fields, cover image URL) to your endpoint.
The response status appears inline — green means your endpoint accepted it.
Common issues
Endpoint not reachable — verify your URL is publicly accessible over HTTPS with a valid SSL certificate. Test it with curl from another machine.
Auth failures — check for trailing whitespace in your token. Header names are case-sensitive on some servers.
Timeout errors — your endpoint must respond within 30 seconds. Move image processing, database writes, and rendering to a background job.
Image not showing — main_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.
Duplicate posts — store article.id and use it to upsert instead of always inserting.
Popular setups
Your own CMS — build a /api/webhooks/blog route, read article.title, article.content_markdown (or content_html), main_image, and insert into your database.
Zapier / Make — add a Webhook trigger, then pass article.content_markdown and main_image.url to WordPress, Notion, or any other action step.
n8n — HTTP webhook node receives the payload; downstream nodes handle storage and image upload.
Slack / email notifications — trigger a message whenever a new article is published using article.title and article.slug.
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.