crontent
Integration·Contentful·intermediate

Automated blog posts for Contentful

Automated blog posts for Contentful: receive Crontent post.ready webhooks and create draft entries via the Contentful Management API.

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

Verify the Crontent webhook, fetch the full post, map dual titles and sources, then create a Contentful Entry via the Management API and leave it unpublished for editorial review.

What you need

  • Crontent API key and webhook secret (CRONTENT_API_KEY, CRONTENT_WEBHOOK_SECRET)
  • A Contentful space with a content type (e.g. blogPost) matching your front-end fields
  • Content Management API token with create access
  • A server-side webhook endpoint (Contentful requires HTTPS)

Register your endpoint in Crontent:

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 Contentful

Crontent fieldContentful fieldNotes
titletitleShort text, display heading
seo_titleseoTitleMeta and og:title; fall back to title
slugslugUnique per locale
excerptexcerptShort text or long text
body_mdx / body_htmlbodyRich text or long text field
tagstagsShort text list or taxonomy
hero_image_urlheroImageAsset link or external URL field
canonical_urlcanonicalUrlOptional
sources[]sourcesJSON object array
ready_atcrontentReadyAtDate field for sync tracking

Crontent handles research and first drafts from your brief. You control angle and tone in the brief; entries stay drafts until a human publishes in Contentful.

Receive and verify webhooks

Headers: X-Crontent-Signature, X-Crontent-Timestamp, X-Crontent-Delivery. Retries: 1m, 5m, 30m, 2h, 12h. Use the delivery ID as an idempotency key.

ts
import { Crontent } from "@crontent/sdk";
import contentfulManagement from "contentful-management";

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 seen(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 createContentfulDraft(post);
  await markSeen(deliveryId);
  return new Response("OK");
}

Create a draft in Contentful

Use the Management API. Entries created without publishing remain drafts.

ts
const client = contentfulManagement.createClient({
  accessToken: process.env.CONTENTFUL_MANAGEMENT_TOKEN!,
});
const space = await client.getSpace(process.env.CONTENTFUL_SPACE_ID!);
const env = await space.getEnvironment("master");

async function createContentfulDraft(post: Post) {
  const entry = await env.createEntry("blogPost", {
    fields: {
      title: { "en-US": post.title },
      seoTitle: { "en-US": post.seo_title ?? post.title },
      slug: { "en-US": post.slug },
      excerpt: { "en-US": post.excerpt },
      body: { "en-US": post.body_html ?? post.body_mdx },
      tags: { "en-US": post.tags },
      crontentPostId: { "en-US": post.id },
    },
  });
  // Do not call entry.publish(); leave as draft for review
  return entry;
}

Backfill and polling

Use crontent.posts.list({ projectId, since, cursor }) to catch missed deliveries. Query Contentful by crontentPostId before creating to prevent duplicates.

Official documentation