Skip to content

Webhooks

Webhooks deliver a signed HTTP POST to your endpoint when events occur on a verified website (e.g. its AI node is republished). Webhooks are a Pro and Business feature.

Subscribing

Manage subscriptions from the dashboard → a website → Webhooks, or via the REST API (organization_admin+):

POST /v1/websites/{websiteId}/webhooks
{ "url": "https://example.com/hooks/bainquet", "events": ["node.published"] }

The response includes a signing secret, shown exactly once — store it now; it cannot be retrieved later (only its hash is kept). Rotate by deleting and recreating the subscription.

Events currently emitted: node.published, verification.lost, verification.restored.

Delivery

Each delivery is an HTTP POST with a JSON body and these headers:

HeaderMeaning
X-Bainquet-Eventthe event type (e.g. node.published)
X-Bainquet-Event-Idstable id of the source event (use it to deduplicate)
X-Bainquet-Deliveryunique id of this delivery attempt
X-Bainquet-Signaturet=<unix>,v1=<hex HMAC-SHA256> (see below)

Delivery is at-least-once — the same X-Bainquet-Event-Id may arrive more than once (retries). Make your handler idempotent on that id. Respond 2xx quickly; non-2xx (or a timeout) is retried with exponential backoff, and a persistently failing endpoint is eventually disabled.

Verifying the signature

Important — the HMAC key is sha256(secret), not the secret itself. bAInquet stores only the SHA-256 hash of your signing secret, and signs with that hash. So your verifier must hash the one-time secret first, then HMAC.

The signed message is "{t}.{rawBody}" — the exact raw request body bytes, not a re-serialized copy. Reject if t is older than ~5 minutes (replay protection).

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

// `secret` = the value shown ONCE when you created the webhook.
function verify(rawBody, signatureHeader, secret) {
  const parts = Object.fromEntries(
    signatureHeader.split(",").map((kv) => kv.split("=")),
  );
  const t = parts.t;
  const v1 = parts.v1;
  if (!t || !v1) return false;
  // Replay window: 5 minutes.
  if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return false;

  // KEY = sha256(secret) as a HEX STRING — bAInquet stores the secret's hash as
  // hex (secret_hash) and HMACs with that hex string as the key, so you must use
  // the HEX digest here (NOT the raw 32-byte Buffer).
  const { createHash } = require("node:crypto");
  const hashedKeyHex = createHash("sha256").update(secret, "utf8").digest("hex");
  const expected = createHmac("sha256", hashedKeyHex)
    .update(`${t}.${rawBody}`, "utf8")
    .digest("hex");

  const a = Buffer.from(expected, "hex");
  const b = Buffer.from(v1, "hex");
  return a.length === b.length && timingSafeEqual(a, b);
}

In other languages the recipe is the same: key = SHA256_HEX(secret) (the lowercase hex string, not raw bytes), then signature = HMAC_SHA256_HEX(key, t + "." + rawBody), compared in constant time.

Owner-controlled structured data for AI.