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 POST request.
  • 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 2xx status.
  • 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: Bearer header against the token you set in PrismSEO. Reject anything else with 401.
  • 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:

  • eventarticle.published when you publish live, article.drafted when you publish as a draft, or test when 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 styles content_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_html comes from your own authenticated PrismSEO account, so rendering it with dangerouslySetInnerHTML is safe here. Never render content_html from 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_TOKEN in your app's environment.
  • Click Save, then Send test event. Your endpoint should receive event: "test" and return 200.

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 Historypublished on success, failed with 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

  • 401 in 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 the 200.
  • 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.