Documentation
Webhook Integration
Publish PrismSEO articles to any stack — Next.js, headless CMS, or internal pipeline — via a signed webhook, with a full payload contract and code.
Webhook Integration
The webhook integration lets PrismSEO publish a finished article to any stack — a Next.js app, a headless CMS, a serverless function, or an internal pipeline. When you click Publish → Webhook, PrismSEO sends a single authenticated POST request to a URL you control. Everything after that is your code.
Use this integration when you don't run WordPress or Framer, or when you want full control over how content is stored, transformed, and rendered.
How it works
The flow is deliberately simple — one request, one response:
- You build an HTTPS endpoint that accepts a
POSTrequest. - You add its URL and a secret token in PrismSEO.
- On publish, PrismSEO sends the article as JSON, authenticated with your token.
- Your endpoint stores or renders the article and replies with a
2xxstatus. - PrismSEO records the outcome in Publish History.
PrismSEO waits up to 15 seconds for your endpoint to respond. Do the slow work (image processing, deploys, re-indexing) after you reply 200, not before.
The request PrismSEO sends
Every publish is one HTTPS POST with these headers:
POST /api/prismseo-webhook HTTP/1.1
Content-Type: application/json
Authorization: Bearer <your-webhook-token>
User-Agent: PrismSEO-Webhook/1.0
Two rules that matter:
- Authenticate every request by checking the
Authorization: Bearerheader against the token you set in PrismSEO. Reject anything else with401. - HTTPS only. PrismSEO blocks private, localhost, and non-HTTPS URLs.
The payload
The body is JSON with a top-level event and a nested article object:
{
"event": "article.published",
"integration_name": "My Production Site",
"article": {
"id": "a1b2c3d4-0000-4444-8888-abcdef123456",
"title": "10 Proven Ways to Speed Up Your Site",
"slug": "speed-up-your-site",
"content_html": "<h1>10 Proven Ways...</h1><p>...</p>",
"excerpt": "A practical, tested checklist for a faster website.",
"meta_title": "10 Proven Ways to Speed Up Your Site (2026)",
"meta_description": "A practical, tested checklist for a faster website.",
"published_at": "2026-07-31T09:15:00.000Z",
"prism_css": ".prism-article h2 { font-size: 1.5rem; }"
}
}
Field reference:
event—article.publishedwhen you publish live,article.draftedwhen you publish as a draft, ortestwhen you send a test event from the dashboard.integration_name— the label you gave this connection in PrismSEO.article.id— a stable UUID. Use it as your idempotency key so retries never create duplicates.article.title— the H1 / display title.article.slug— a URL-safe slug, already de-duplicated.article.content_html— the full article body as HTML (headings, paragraphs, images, tables, FAQ).article.excerpt— a short summary (falls back to the meta description).article.meta_title— the SEO title tag.article.meta_description— the meta description.article.published_at— an ISO 8601 timestamp of when this event was sent.article.prism_css— optional CSS that stylescontent_html. Include it once on the page if you don't already style article HTML.
Step 1 — Build a receiver (Next.js App Router)
Create a route handler that validates the token, reads the payload, and responds fast. Store the raw article, then do heavy work asynchronously.
// app/api/prismseo-webhook/route.ts
import { NextRequest, NextResponse } from 'next/server'
const PRISM_TOKEN = process.env.PRISMSEO_WEBHOOK_TOKEN!
export async function POST(req: NextRequest) {
// 1. Authenticate every request.
const auth = req.headers.get('authorization') ?? ''
if (auth !== `Bearer ${PRISM_TOKEN}`) {
return NextResponse.json({ error: 'unauthorized' }, { status: 401 })
}
// 2. Parse the payload.
const { event, article } = await req.json()
if (!article?.id || !article?.content_html) {
return NextResponse.json({ error: 'invalid payload' }, { status: 400 })
}
// 3. Upsert by article.id so retries are idempotent (no duplicates).
await db.posts.upsert({
where: { prismId: article.id },
create: {
prismId: article.id,
slug: article.slug,
title: article.title,
html: article.content_html,
excerpt: article.excerpt,
metaTitle: article.meta_title,
metaDescription: article.meta_description,
status: event === 'article.published' ? 'published' : 'draft',
publishedAt: article.published_at,
},
update: {
title: article.title,
html: article.content_html,
status: event === 'article.published' ? 'published' : 'draft',
},
})
// 4. Reply within 15s. Trigger revalidation / deploys AFTER this returns.
return NextResponse.json({ ok: true }, { status: 200 })
}
Render the stored article on your blog route, injecting prism_css once so the HTML is styled:
// app/blog/[slug]/page.tsx
export default async function Post({ params }: { params: { slug: string } }) {
const post = await db.posts.findBySlug(params.slug)
if (!post) notFound()
return (
<article className="prism-article">
<style dangerouslySetInnerHTML={{ __html: post.prismCss ?? '' }} />
<div dangerouslySetInnerHTML={{ __html: post.html }} />
</article>
)
}
content_htmlcomes from your own authenticated PrismSEO account, so rendering it withdangerouslySetInnerHTMLis safe here. Never rendercontent_htmlfrom an unauthenticated source.
Step 2 — Connect the webhook in PrismSEO
- Open Dashboard → Integrations → Webhook / Custom CMS.
- Enter your destination URL (for example
https://yoursite.com/api/prismseo-webhook). - Enter an access token — any long random string. Set the same value as
PRISMSEO_WEBHOOK_TOKENin your app's environment. - Click Save, then Send test event. Your endpoint should receive
event: "test"and return200.
Step 3 — Publish an article
- Open any article in Content.
- Click Publish → Webhook.
- Choose Draft (
article.drafted) or Publish (article.published). - The result appears in Publish History —
publishedon success,failedwith the error message otherwise.
Test it with curl
Simulate exactly what PrismSEO sends before wiring up a real publish:
curl -X POST https://yoursite.com/api/prismseo-webhook \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $PRISMSEO_WEBHOOK_TOKEN" \
-H "User-Agent: PrismSEO-Webhook/1.0" \
-d '{
"event": "test",
"integration_name": "Local test",
"article": {
"id": "test-123",
"title": "Hello from PrismSEO",
"slug": "hello-from-prismseo",
"content_html": "<h1>Hello</h1><p>It works.</p>",
"excerpt": "Test event",
"meta_title": "Hello",
"meta_description": "Test event",
"published_at": "2026-07-31T09:15:00.000Z"
}
}'
A correctly configured endpoint returns {"ok":true} with status 200.
Production best practices
- Validate the token on every request. Never trust an unauthenticated POST.
- Be idempotent. Upsert on
article.id; a retried publish must update, not duplicate. - Reply in under 15 seconds. Queue slow work (deploys, image CDN, re-indexing) and run it after you respond.
- Persist first, process later. Store the raw payload immediately so a downstream failure never loses the article.
- Return honest status codes.
2xx= accepted,4xx= bad payload/auth (won't be retried),5xx= your side failed (safe to retry). - Log the response body on failures so repeated issues are debuggable from Publish History.
Troubleshooting
401in Publish History — the token in PrismSEO doesn't match your endpoint's expected token. Re-enter both sides.Request timed out after 15s— your handler is doing slow work before responding. Move it after the200.Endpoint returned 4xx/5xx— your handler rejected or crashed on the payload. Check your logs and the field reference above.- Nothing arrives — the URL is wrong, not public HTTPS, or blocked. PrismSEO refuses private/localhost URLs; expose a real HTTPS endpoint (a tunnel like ngrok works for local testing).
For the exact contract shared across all custom integrations, see the Publishing API Reference.
Next step
Apply this setup, then run your first publish cycle from your dashboard.