Automatización

Webhooks

A webhook is a request CMSKite sends to your URL when something happens. The common use: refresh a cached site the moment a post is published, instead of waiting for a rebuild.

Actualizado el 28 de octubre de 2026

2 min de lectura

Add one

Open Developers → Webhooks, enter your URL and pick the events. Or over the API:

MétodoRutaQué haceAutenticación
POST/v1/projects/{projectId}/webhooksAdd a webhook: { url, events }Session
The signing secret is shown once, when you add the webhook. Store it in your site's environment. Afterwards the list shows only its last four characters.

Events

EventSent when
post.publishedA post goes live, including a scheduled one
post.updatedA live post is changed
post.unpublishedA live post is set back to draft
post.deletedA post is deleted
visibility.score_droppedThe website's visibility score falls
visibility.critical_issueA critical problem is found on the website
report.dailyThe daily report is ready
content.run.completedThe content autopilot finished a run
content.run.failedA content autopilot run failed

What you receive

json
{
  "id": "dlv_…",
  "event": "post.published",
  "occurredAt": "2026-10-26T09:30:00.000Z",
  "projectId": "…",
  "data": { "id": "…", "slug": "my-first-post", "status": "published" }
}

Post events carry the id, slug and status only. Fetch the post with your own key if you need its content: the webhook never sends text that a stale or replayed request could overwrite.

HeaderValue
CMSKite-Signaturet=<unix seconds>,v1=<hex HMAC-SHA256>
CMSKite-Delivery-IdThe same on every retry of one delivery
CMSKite-EventThe event name
User-AgentCMSKite-Webhooks/1.0

Check the signature

Sign <t>.<raw body> with your secret and compare. Reject a timestamp older than five minutes, so a captured request cannot be replayed:

verify.ts

ts
import { createHmac, timingSafeEqual } from 'node:crypto'

export function verify(rawBody: string, header: string, secret: string) {
  const parts = Object.fromEntries(header.split(',').map((p) => p.split('=')))
  const t = Number(parts.t)
  if (!t || Math.abs(Date.now() / 1000 - t) > 300) return false
  const expected = createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex')
  const given = Buffer.from(parts.v1 ?? '', 'hex')
  return given.length === 32 && timingSafeEqual(given, Buffer.from(expected, 'hex'))
}
Use the raw body as received. Parsing and re-serialising the JSON changes the bytes and the signature no longer matches.

Example: refresh a Next.js site

app/api/cmskite/route.ts

ts
import { revalidatePath } from 'next/cache'
import { verify } from '@/lib/verify'

export async function POST(req: Request) {
  const body = await req.text()
  if (!verify(body, req.headers.get('cmskite-signature') ?? '', process.env.CMSKITE_WEBHOOK_SECRET!)) {
    return new Response('bad signature', { status: 401 })
  }
  const { data } = JSON.parse(body)
  revalidatePath('/blog')
  if (data?.slug) revalidatePath(`/blog/${data.slug}`)
  return new Response('ok')
}

Static sites (Astro, Gatsby, Nuxt generate) can point the webhook at their host's deploy hook instead, if it accepts a POST.

Delivery and retries

  • Answer with any 2xx within 10 seconds. Do slow work after answering
  • Redirects are not followed: use the final URL
  • A failed delivery is tried again after 10, 20, 40, 80 and 160 seconds: six attempts over about five minutes
  • The URL must be public. Private and internal addresses are refused
  • A retry has the same CMSKite-Delivery-Id. Store the ids you handled and skip repeats