ClimbrIQDeveloper docs
Flexible

Deliver articles to any CMS via webhook

When an article is published, ClimbrIQ can send a JSON payload to your endpoint. Use it to create posts in a headless CMS, trigger an internal workflow, or feed a custom publishing pipeline.

How it works

Once enabled for a brand, ClimbrIQ makes an HTTP POST request to your webhook URL each time an article is published, and again each time a published article is refreshed. The event field tells you which: publish for a new article, update for a new version of one you already have. Match updates to the original post by articleId.

ClimbrIQ treats any 2xx response as success. Anything else is a failed delivery. A failed publish leaves the article unpublished in ClimbrIQ; a failed update is still saved in ClimbrIQ. Scheduled deliveries are retried automatically with backoff, and the owner is emailed if they keep failing.

Request format

Method

POST

Content type

application/json

Headers

  • Content-Type — always application/json
  • X-Webhook-Secret — the secret configured for the brand. Omitted when no secret is set.
  • X-Idempotency-Key — for a publish, the same on every retry, so you can ignore a key you have already processed. Each update gets a new key (a retried update may arrive with a different one), so always apply updates by articleId — applying the same update twice is harmless.

Payload (example)

{
  "event": "publish",
  "articleId": "3f1c2a9e-5b7d-4c1e-9a2b-8d6f0e4c7b21",
  "title": "How to Choose a Local Plumber",
  "metaTitle": "How to Choose a Local Plumber in the UK",
  "metaDescription": "What to check before you book, from registration and insurance to written quotes.",
  "content": "<article body as markdown, without the title>",
  "html": "<the same body rendered to HTML>",
  "slug": "how-to-choose-a-local-plumber",
  "excerpt": "A concise one-sentence summary of the article.",
  "featureImageUrl": "https://… or null"
}
  • title is the article's headline. It is not repeated in content or html, so render it as the post title.
  • metaTitle and metaDescription are for your SEO title and description fields.
  • Use content if your CMS takes markdown, or html if it takes HTML. Both are ready to publish as they are.

Authentication

When you set a secret, ClimbrIQ sends it as X-Webhook-Secret with every request. Validate it on your endpoint before processing the payload.

const secret = request.headers.get('X-Webhook-Secret');
if (secret !== process.env.EXPECTED_SECRET) {
  return new Response('Forbidden', { status: 403 });
}

Example receiver (Next.js Route Handler)

// app/api/climbriq-webhook/route.ts
import { NextResponse } from 'next/server';

export async function POST(request: Request) {
  const secret = request.headers.get('X-Webhook-Secret');
  if (secret !== process.env.CLIMBRIQ_WEBHOOK_SECRET) {
    return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
  }

  // A retry of a delivery you already handled: acknowledge and stop.
  const key = request.headers.get('X-Idempotency-Key');
  if (key && (await yourStore.hasProcessed(key))) {
    return NextResponse.json({ received: true });
  }

  const body = (await request.json()) as {
    event: 'publish' | 'update';
    articleId: string;
    title: string;
    metaTitle: string;
    metaDescription: string;
    content: string;
    html: string;
    slug: string;
    excerpt: string;
    featureImageUrl: string | null;
  };

  const post = {
    title: body.title,
    body: body.html, // or body.content if your CMS takes markdown
    slug: body.slug,
    excerpt: body.excerpt,
    seoTitle: body.metaTitle,
    seoDescription: body.metaDescription,
    imageUrl: body.featureImageUrl,
  };

  // Keyed on articleId, so a refresh updates the post instead of duplicating it.
  const existing = await yourCms.findPostByExternalId(body.articleId);
  if (existing) {
    await yourCms.updatePost(existing.id, post);
  } else {
    await yourCms.createPost({ ...post, externalId: body.articleId });
  }

  if (key) await yourStore.markProcessed(key);
  return NextResponse.json({ received: true });
}

Want the easiest setup?

If you’re on WordPress, the plugin-based integration is usually the fastest route.

See WordPress setup →