RootSmith Docs

Webhooks

Receive each published page as a signed JSON request and save it in any stack. Verify the signature and render the version marker.

Use a webhook when your site isn't on Next.js, or when your own code must save each page, such as into a CMS or a Git repository.

Set it up

  1. Build an HTTPS endpoint that accepts POST requests. It must be public and have no query string.
  2. In Publishing, leave the Delivery endpoint empty. Under Without an SDK connection, choose Webhook, paste the URL, and save.
  3. Select Set up SDK → Get connection secret and add it to your server environment as ROOTSMITH_CONNECTION_SECRET.

An agent sets this with update_publishing_settings, using fallbackDelivery: "webhook" and the URL in webhookUrl. Only a person can copy the secret.

The request

RootSmith sends one request for each publication.

POST /your/webhook HTTP/1.1
content-type: application/json
x-rootsmith-event: page.published
x-rootsmith-time: 1767225600000
x-rootsmith-signature: 3f5a...e91c
{
  "event": "page.published",
  "siteId": "00000000-0000-0000-0000-000000000000",
  "contentId": "11111111-1111-1111-1111-111111111111",
  "version": 4,
  "url": "https://example.com/blog/ai-search-guide",
  "path": "/blog/ai-search-guide",
  "title": "A guide to AI search",
  "description": "How AI assistants choose which brands to name.",
  "markdown": "## Why it matters\n\n...",
  "sentAt": "2026-01-01T00:00:00.000Z"
}
FieldDescription
eventAlways page.published.
siteIdThe RootSmith site ID.
contentIdThe page ID. Use it for the version marker.
versionThe version number. Use it for the version marker.
urlThe page's full URL on your domain.
pathThe page path, such as /blog/ai-search-guide.
titleThe page title.
descriptionThe meta description. It can be empty.
markdownThe page body in Markdown, without the title.
sentAtWhen RootSmith sent the request, in ISO 8601.

Any 2xx response counts as delivered. Other responses fail the publication, and nothing on your site changes.

Verify the signature

x-rootsmith-time is the send time in Unix milliseconds. x-rootsmith-signature is the hex HMAC-SHA256 of this string, keyed with the connection secret:

request\n{x-rootsmith-time}\n{raw request body}

Use the raw body, before JSON parsing. Reject a time more than 60 seconds from your clock.

verify.ts
import { createHmac, timingSafeEqual } from "node:crypto"

export function verifyRootSmith(
  body: string,
  time: string | null,
  signature: string | null
) {
  const secret = process.env.ROOTSMITH_CONNECTION_SECRET
  if (!secret || !time || !signature || !/^\d{13}$/.test(time)) return false
  if (Math.abs(Date.now() - Number(time)) > 60_000) return false
  const expected = createHmac("sha256", secret)
    .update(`request\n${time}\n${body}`)
    .digest()
  const given = Buffer.from(signature, "hex")
  return given.length === expected.length && timingSafeEqual(given, expected)
}
app/api/rootsmith-webhook/route.ts
import { verifyRootSmith } from "./verify"

export async function POST(request: Request) {
  const body = await request.text()
  if (
    !verifyRootSmith(
      body,
      request.headers.get("x-rootsmith-time"),
      request.headers.get("x-rootsmith-signature")
    )
  )
    return new Response("Invalid signature", { status: 401 })
  const page = JSON.parse(body)
  await savePage(page)
  return new Response(null, { status: 204 })
}

Show the version marker

RootSmith marks the page published when it finds this element on https://<your domain><path>:

<div
  data-rootsmith-content="{contentId}"
  data-rootsmith-version="{version}"
  hidden
></div>

Render it from the payload's contentId and version. It can be any element. Without it, confirm the page by hand in RootSmith. A title match doesn't count, because an update keeps its title.

RootSmith checks right after delivery, then every five minutes for 24 hours.

On this page