Skip to main content
Integrations
3 min read

Custom-site webhook integration

Connect any site that isn't WordPress/Webflow/Shopify via a signed webhook.

Last updated May 27, 2026

For any tenant whose site isn't WordPress, Webflow, or Shopify — static sites (Hugo, Jekyll, Astro, Next.js), Wix/Squarespace with a custom function, headless CMSes, in-house CMS, or anything that can expose an HTTP endpoint — connect via a signed webhook.

How it works

  1. You build a tiny endpoint on your site that accepts POST with Content-Type: application/json.
  2. You verify the X-AID-Signature header (HMAC-SHA256, Stripe-compatible format).
  3. Your endpoint persists the content into your CMS / Git repo / blog database, and returns 2xx.
  4. (Optional) Include { "url": "..." } in the response body to land the published URL on the ContentItem.

We POST one payload per publish. Same payload shape for every content type (Article, FAQ, Comparison, Location Page, Service Page).

The payload

{
  "v": "1",
  "action": "publish",
  "timestamp": "2026-05-27T03:14:00.000Z",
  "contentItemId": "ckxyz...",
  "content": {
    "type": "ARTICLE",
    "title": "How to choose a tax-resolution firm",
    "slug": "how-to-choose-a-tax-resolution-firm",
    "bodyMarkdown": "## The short answer...",
    "metaTitle": "How to choose a tax-resolution firm (2026)",
    "metaDescription": "A practical 5-minute guide...",
    "schemaJsonLd": { "@context": "https://schema.org", "@type": "Article", ... },
    "targetPrompts": ["best tax resolution firm", "how to pick a tax-resolution attorney"],
    "cta": "Schedule a free consult."
  }
}

Signature verification

X-AID-Signature header looks like:

X-AID-Signature: t=1716774840,v1=8f2a5b...64a3e1

To verify in your receiver:

  1. Parse t (unix seconds) and v1 (hex).
  2. Reject if abs(now - t) > 300 seconds (replay protection).
  3. Recompute hmac_sha256(${t} + "." + rawRequestBody, secret).
  4. Compare with v1 using a timing-safe equal.

Node.js reference receiver

import { createHmac, timingSafeEqual } from "node:crypto";

const SECRET = process.env.AID_WEBHOOK_SECRET;

export async function POST(req) {
  const raw = await req.text();
  const sig = Object.fromEntries(
    (req.headers.get("x-aid-signature") ?? "")
      .split(",")
      .map((p) => p.split("=").map((s) => s.trim()))
  );
  const t = Number(sig.t);
  if (!Number.isFinite(t) || Math.abs(Date.now() / 1000 - t) > 300) {
    return new Response("stale", { status: 400 });
  }
  const expected = createHmac("sha256", SECRET).update(`${t}.${raw}`).digest("hex");
  const a = Buffer.from(expected, "hex");
  const b = Buffer.from(sig.v1 ?? "", "hex");
  if (a.length !== b.length || !timingSafeEqual(a, b)) {
    return new Response("bad signature", { status: 401 });
  }

  const payload = JSON.parse(raw);
  if (payload.action === "test") {
    return Response.json({ ok: true });
  }

  // Persist content however your CMS / static site / Git repo wants it.
  // Return { url: "..." } so it lands on the ContentItem.
  const url = await persistToYourCms(payload.content);
  return Response.json({ url });
}

Python (FastAPI) reference receiver

import hashlib, hmac, time, os
from fastapi import FastAPI, Header, HTTPException, Request

SECRET = os.environ["AID_WEBHOOK_SECRET"].encode()
app = FastAPI()

@app.post("/webhook")
async def webhook(req: Request, x_aid_signature: str = Header(...)):
    raw = (await req.body()).decode()
    parts = dict(p.strip().split("=", 1) for p in x_aid_signature.split(","))
    t = int(parts["t"])
    if abs(time.time() - t) > 300:
        raise HTTPException(400, "stale")
    expected = hmac.new(SECRET, f"{t}.{raw}".encode(), hashlib.sha256).hexdigest()
    if not hmac.compare_digest(expected, parts.get("v1", "")):
        raise HTTPException(401, "bad signature")
    payload = await req.json()
    if payload.get("action") == "test":
        return {"ok": True}
    url = persist_to_your_cms(payload["content"])
    return {"url": url}

GitHub Actions reference (for static sites)

If your site is built from a Git repo (Hugo, Jekyll, Astro, Next.js static), use a tiny serverless function as the webhook receiver. It verifies the signature, writes a Markdown file to your repo, and triggers a deploy.

The pattern is identical to the Node.js example above; the persistence step is git commit + git push (via the GitHub REST API, the Octokit SDK, or your platform's "trigger build" webhook).

Configuration

In the AI Domination dashboard:

  1. Open a company → PublishingCustom (webhook) tab.
  2. Paste your webhook URL and a 32+ character signing secret.
  3. Click Test & connect. We POST a content.test event; we only save the configuration if your receiver replies 2xx with a valid signature.

Generate a secret with openssl rand -hex 32 and paste the same value into your receiver's environment.

What we send

Every publish triggers exactly one POST with action: "publish". We don't retry on 5xx (you'll see the failure in the ContentItem's status — re-publish from the dashboard). 4xx is treated as a permanent failure.

What we expect back

Any 2xx status code = success. Optional JSON body with the published URL helps but isn't required. We tolerate text/plain responses (just say "ok") for receivers that don't want to round-trip JSON.

See also

Was this article helpful?

Related docs

Custom-site webhook integration · AI Domination