Webhooks

Webhooks let an external frontend revalidate when your content changes. Floggy POSTs a small payload to your endpoint whenever a post is published or updated, so you can purge a cache or trigger an ISR revalidation instead of polling.

Delivery is best-effort and fire-and-forget: it never blocks your publish, and a failed delivery is recorded on the endpoint (last status, last fired) but not retried.

Events

Event Fires when
post.published A post goes from draft to published, or is created already published.
post.updated An already-published post is edited.

Notes:

  • Drafts never fire anything.
  • Switching a post back to draft fires nothing.
  • Known limitation: scheduled (future-dated) posts do not fire at write time. The event only fires when the post is live now, so a post scheduled for next week will not emit post.published when that date arrives.

Managing endpoints

You create and manage webhook endpoints in the Dashboard (Settings -> Developer). Add an endpoint URL (the scheme auto-adds https:// when omitted), pick the events to subscribe to, and toggle delivery on or off. Up to 10 endpoints per account.

When you create an endpoint, the signing secret (whsec_...) is shown once. Store it now - it is used to verify deliveries and is not shown again. Keep it in an env var such as FLOGGY_WEBHOOK_SECRET on the app that receives the webhook.

Delivery payload

Floggy POSTs JSON to your endpoint:

{
  "event": "post.published",
  "post": {
    "id": "V1StGXR8_Z5jdHi6B-myT",
    "slug": "launch-day",
    "title": "Launch Day",
    "publishedAt": "2026-06-29T10:00:00.000Z",
    "updatedAt": "2026-06-29T10:00:00.000Z",
    "url": "https://projecta.floggy.xyz/launch-day"
  },
  "timestamp": "2026-06-29T10:00:00.123Z"
}

Headers on the request:

  • Content-Type: application/json
  • User-Agent: Floggy-Webhooks/1.0
  • X-Floggy-Signature: sha256=<hex>

Verifying the signature

X-Floggy-Signature is sha256= followed by the HMAC-SHA256 of the raw request body, keyed with your endpoint secret, lowercase hex. Recompute it over the exact bytes you received (do not re-serialize the parsed JSON) and compare in constant time.

// Web Crypto. Works in Workers, Deno, Bun, Node 18+, and the browser.
// Pass the RAW request body string and the X-Floggy-Signature header value.
export async function verifyFloggySignature(
  rawBody: string,
  signatureHeader: string | null,
  secret: string,
): Promise<boolean> {
  if (!signatureHeader) return false;

  const expectedHex = signatureHeader.startsWith("sha256=")
    ? signatureHeader.slice("sha256=".length)
    : signatureHeader;

  const enc = new TextEncoder();
  const key = await crypto.subtle.importKey(
    "raw",
    enc.encode(secret),
    { name: "HMAC", hash: "SHA-256" },
    false,
    ["sign"],
  );
  const sigBuf = await crypto.subtle.sign("HMAC", key, enc.encode(rawBody));
  const actualHex = [...new Uint8Array(sigBuf)]
    .map((b) => b.toString(16).padStart(2, "0"))
    .join("");

  return timingSafeEqual(actualHex, expectedHex);
}

// Constant-time string compare: never short-circuit on the first mismatch.
function timingSafeEqual(a: string, b: string): boolean {
  if (a.length !== b.length) return false;
  let diff = 0;
  for (let i = 0; i < a.length; i++) {
    diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
  }
  return diff === 0;
}

Next.js route handler example

// app/api/floggy-webhook/route.ts
import { verifyFloggySignature } from "@/lib/floggy-webhook";
import { revalidatePath } from "next/cache";

export async function POST(req: Request) {
  const raw = await req.text(); // read raw body BEFORE JSON.parse
  const ok = await verifyFloggySignature(
    raw,
    req.headers.get("X-Floggy-Signature"),
    process.env.FLOGGY_WEBHOOK_SECRET!,
  );
  if (!ok) return new Response("invalid signature", { status: 401 });

  const { event, post } = JSON.parse(raw) as {
    event: string;
    post: { slug: string };
  };

  revalidatePath(`/${post.slug}`);
  revalidatePath("/");
  return new Response("ok");
}

Reject any request whose signature does not match. The signature is the only proof the request came from Floggy and not a spoofed POST to your public endpoint.