Start free. Scale as your blog grows.Get started

API reference

Webhooks

Publishing webhooks let ContioReach notify your application when content changes.

This is especially useful when your frontend uses cached or statically generated pages. Instead of repeatedly checking whether content changed, your application can refresh the relevant pages when ContioReach sends a webhook.

Common use case

A typical publishing flow looks like this:

  1. Content is published in ContioReach
  2. ContioReach sends a webhook
  3. Your application verifies the secret
  4. The blog index is revalidated
  5. The article page is revalidated

Configure your webhook

Inside your workspace:

  1. 1Open Developer Settings
  2. 2Select Webhook Integration
  3. 3Enter the URL for your deployed application
  4. 4Generate a secure token
  5. 5Save the webhook configuration

Store the generated token in your application:

.env.local
REVALIDATION_SECRET=your_generated_secret_token

When webhooks are sent

ContioReach sends one POST request to your webhook URL when:

  • A post is published
  • A post is scheduled
  • A published post is edited
  • A post is deleted

Saving a draft does not send a webhook.

Request

Headers
POST https://yoursite.com/api/revalidate
Content-Type: application/json
X-API-Key: your_generated_secret_token

The secret is sent twice: in the X-API-Key header and as secret in the body. Check either one before acting on the request.

Payload

Body
{
  "secret": "your_generated_secret_token",
  "post": {
    "id": "b7e1f2a3-0000-4000-8000-000000000001",
    "title": "Getting Started With Your First Blog Post",
    "metaTitle": "Getting Started With Your First Blog Post | Your Blog",
    "slug": "getting-started-first-blog-post",
    "description": "The meta description used for search engines.",
    "coverImage": "https://contiocdn.com/images/getting-started-cover.webp",
    "coverImageAlt": "Laptop open on a desk with a blank blog draft",
    "excerpt": "A short summary of the post, shown on listing pages.",
    "content": "<h2>Introduction</h2><p>Your post content as HTML…</p>",
    "publishedAt": "2026-01-15T09:30:00.000Z",
    "createdAt": "2026-01-14T16:05:00.000Z",
    "updatedAt": "2026-01-20T11:12:00.000Z",
    "workspaceId": "e5f6a7b8-0000-4000-8000-000000000001",
    "userId": "f6a7b8c9-0000-4000-8000-000000000001",
    "primaryKeyword": "first blog post",
    "secondaryKeywords": ["how to start a blog", "blog post template"]
  }
}
FieldTypeDescription
secretstringYour revalidation secret. Reject the request if it does not match.
post.idstringUnique identifier of the post.
post.titlestringArticle title.
post.metaTitlestringSEO title for <title> and og:title. Falls back to title when not set.
post.slugstringURL-safe identifier. Use it to revalidate the article route.
post.descriptionstring | nullMeta description.
post.coverImagestring | nullAbsolute URL of the cover image.
post.coverImageAltstringAlt text for the cover image. Falls back to title when not set.
post.excerptstring | nullShort summary for cards and listings.
post.contentstring | nullArticle body as HTML.
post.publishedAtstring | nullISO 8601 publication timestamp.
post.createdAtstringISO 8601 creation timestamp.
post.updatedAtstringISO 8601 last-modified timestamp.
post.workspaceIdstringThe workspace the post belongs to.
post.userIdstringThe user who created the post.
post.primaryKeywordstring | nullTarget keyword for the article.
post.secondaryKeywordsstring[]Supporting keywords. Empty array when none are set.

The payload does not say which event happened. On delete, post is the post as it was just before removal. Revalidating post.slug handles every case, because the Content API only serves published posts.

Responding

Respond with HTTP 200 within 15 seconds. Any other status — including 201, 202, or 204 — is recorded as a failed delivery.

Next.js revalidation example

Create:

Path
app/api/revalidate/route.ts
app/api/revalidate/route.ts
import { revalidatePath } from "next/cache";

export async function POST(request: Request) {
  const { secret, post } = await request.json();

  if (
    secret !==
    process.env.REVALIDATION_SECRET
  ) {
    return Response.json(
      { error: "Invalid token" },
      { status: 401 }
    );
  }

  revalidatePath("/blog");

  if (post?.slug) {
    revalidatePath(`/blog/${post.slug}`);
  }

  return Response.json({
    revalidated: true
  });
}

When ContioReach sends the webhook, your application verifies the secret before revalidating content.

What gets refreshed

The example refreshes:

Path
/blog

and, when the payload includes a post slug:

Path
/blog/[slug]

This keeps both the blog listing and the individual article current.

Keep your webhook secret private

The webhook secret should be treated like a credential. Do not expose it in browser code or commit it to a public repository.

Always verify the secret before performing revalidation.

Was this page helpful?