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.
Add one
Open Developers → Webhooks, enter your URL and pick the events. Or over the API:
| Método | Caminho | O que faz | Autenticação |
|---|---|---|---|
| POST | /v1/projects/{projectId}/webhooks | Add a webhook: { url, events } | Session |
Events
| Event | Sent when |
|---|---|
post.published | A post goes live, including a scheduled one |
post.updated | A live post is changed |
post.unpublished | A live post is set back to draft |
post.deleted | A post is deleted |
visibility.score_dropped | The website's visibility score falls |
visibility.critical_issue | A critical problem is found on the website |
report.daily | The daily report is ready |
content.run.completed | The content autopilot finished a run |
content.run.failed | A content autopilot run failed |
What you receive
{
"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.
| Header | Value |
|---|---|
CMSKite-Signature | t=<unix seconds>,v1=<hex HMAC-SHA256> |
CMSKite-Delivery-Id | The same on every retry of one delivery |
CMSKite-Event | The event name |
User-Agent | CMSKite-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
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'))
}Example: refresh a Next.js site
app/api/cmskite/route.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







