crontent
Integration·Strapi·beginner

Scheduled blog posts for Strapi

Scheduled blog posts for Strapi: deliver Crontent drafts into Strapi over signed webhooks and the REST API for editorial review.

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

Receive the signed Crontent webhook, fetch the post, map fields into a Strapi article via the REST API, and keep status unpublished until someone approves it.

What you need

  • CRONTENT_API_KEY and CRONTENT_WEBHOOK_SECRET from your Crontent project
  • Strapi v4+ with an article (or post) collection type
  • A Strapi API token with create permissions on that collection
  • A webhook receiver with access to both APIs

Register the webhook:

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 Strapi

Crontent fieldStrapi fieldNotes
titletitleString, display H1
seo_titleseoTitleMeta title; fall back to title
slugslugUID or string field
excerptdescriptionText or richtext
body_mdx / body_htmlcontentRichtext or markdown field
tagstagsRelation or JSON
hero_image_urlcoverMedia or URL string
canonical_urlcanonicalUrlOptional string
sources[]sourcesJSON component or repeatable
ready_atcrontentReadyAtDatetime for sync

Crontent researches and drafts from your brief; you set the angle. Keep publishedAt null so Strapi treats entries as drafts until review.

Receive and verify webhooks

Crontent retries failed deliveries at 1m, 5m, 30m, 2h, and 12h. Deduplicate using X-Crontent-Delivery.

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 isDuplicate(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 createStrapiDraft(post);
  await recordDelivery(deliveryId);
  return new Response("OK");
}

Create a draft in Strapi

POST to the collection endpoint without setting publishedAt. In Strapi v4, omitting publish fields creates an unpublished entry.

ts
async function createStrapiDraft(post: Post) {
  const res = await fetch(`${process.env.STRAPI_URL}/api/articles`, {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.STRAPI_API_TOKEN}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      data: {
        title: post.title,
        seoTitle: post.seo_title ?? post.title,
        slug: post.slug,
        description: post.excerpt,
        content: post.body_html ?? post.body_mdx,
        tags: post.tags,
        sources: post.sources,
        crontentPostId: post.id,
        // publishedAt intentionally omitted (draft)
      },
    }),
  });
  if (!res.ok) throw new Error(await res.text());
}

Backfill and polling

Poll crontent.posts.list({ projectId, since, limit, cursor }) and skip posts whose crontentPostId already exists in Strapi. Useful after downtime or webhook misconfiguration.

Official documentation