crontent
Integration·Payload·intermediate

AI blog content for Payload CMS

AI blog content for Payload CMS: verify Crontent webhooks and create draft posts with the Local API or REST endpoints.

How-to: Integrate Crontent with Payload CMS. Official docs: Payload. Shared API contract: Publishing API and SDK.

Verify Crontent's webhook, fetch the post, and create a Payload draft with the Local API or REST so review happens before publish.

What you need

  • Crontent credentials: CRONTENT_API_KEY, CRONTENT_WEBHOOK_SECRET
  • Payload CMS with a posts collection (title, slug, rich text body, etc.)
  • Either a Payload Local API hook inside your Next.js app, or REST with an API key
  • @crontent/sdk in your webhook handler

Register the webhook endpoint:

ts
await crontent.webhooks.register({
  projectId: process.env.CRONTENT_PROJECT_ID!,
  url: "https://your-app.com/api/crontent/webhook",
  events: ["post.ready"],
});

Map Crontent fields to Payload

Crontent fieldPayload fieldNotes
titletitleText, display H1
seo_titlemeta.titleSEO plugin or custom group; fall back to title
slugslugAuto-generated or from Crontent
excerptexcerptTextarea
body_mdx / body_htmlcontentRich text or Lexical field
tagstagsArray or relationship
hero_image_urlheroImageUpload or URL
canonical_urlmeta.canonicalURLSEO group
sources[]sourcesArray or JSON field
ready_atcrontentReadyAtDate field

Crontent runs research and produces a first draft from your brief. You steer angle in the brief; Payload drafts await human or AI-assisted review before _status moves to published.

Receive and verify webhooks

Use X-Crontent-Delivery for idempotency. Failed deliveries retry at 1m, 5m, 30m, 2h, 12h.

ts
import { Crontent } from "@crontent/sdk";

const crontent = new Crontent({ apiKey: process.env.CRONTENT_API_KEY! });

export async function POST(req: Request) {
  const rawBody = await req.text();
  const deliveryId = req.headers.get("x-crontent-delivery")!;

  if (await processed(deliveryId)) return new Response("OK");

  const valid = await crontent.webhooks.verify({
    rawBody,
    signature: req.headers.get("x-crontent-signature"),
    timestamp: req.headers.get("x-crontent-timestamp"),
    secret: process.env.CRONTENT_WEBHOOK_SECRET!,
  });
  if (!valid) return new Response("Unauthorized", { status: 401 });

  const { post_id } = JSON.parse(rawBody);
  const post = await crontent.posts.get(post_id);
  await createPayloadDraft(post);
  await markProcessed(deliveryId);
  return new Response("OK");
}

Create a draft in Payload

Prefer the Local API inside a Payload route handler for type safety. Set _status: 'draft' explicitly.

ts
import { getPayload } from "payload";
import config from "@payload-config";

async function createPayloadDraft(post: Post) {
  const payload = await getPayload({ config });

  await payload.create({
    collection: "posts",
    draft: true,
    data: {
      title: post.title,
      slug: post.slug,
      excerpt: post.excerpt,
      content: post.body_html ?? post.body_mdx,
      tags: post.tags.map((t) => ({ tag: t })),
      meta: {
        title: post.seo_title ?? post.title,
        description: post.excerpt,
        canonicalURL: post.canonical_url,
      },
      crontentPostId: post.id,
      _status: "draft",
    },
  });
}

REST alternative: POST /api/posts?draft=true with a Payload API key and the same JSON body.

Backfill and polling

Call crontent.posts.list({ projectId, since, cursor }) and filter out posts already stored by crontentPostId. Run as a cron job or one-off script.

Official documentation