Subscribe to 15 lifecycle events. Every delivery is signed with HMAC-SHA256, retried on transient failures, and timed out at 10 seconds so a flaky receiver can't back up the publish pipeline.
2xx within 10 seconds. Non-2xx responses are logged in the deliveries view and counted toward auto-disable thresholds.15 events across content lifecycle, ideas backlog, campaigns, reviews, AI visibility alerts, and competitor intel. Subscribe to whichever subset you need — the table is generated from the same canonical list the publish workers fire.
content.createdA new content row is inserted (any source).content.scheduledA content row is scheduled for a future publish.content.publishedPublish workers successfully posted to at least one platform.content.failedAll publish attempts failed for a content row after retries.content.recycledAn auto-recycle re-publish completed.content.spikeAn engagement spike-alert fires for a published piece.idea.createdAn idea is captured to Mission Control (UI / API / extension).idea.assignedAn idea's assignee changes (human or agent).campaign.activatedA campaign moves from draft to active.review.approvedEditorial review approves a content row.review.rejectedEditorial review rejects a content row.visibility.alertAI Visibility tracker emits an alert (score_drop / prompt_zeroed / competitor_added).intel.shiftLandscape brief detects a competitor positioning change.intel.pivotCompetitor-pivot detector flags a sustained shift.competitor.postedA tracked competitor's RSS feed publishes a new piece.Content-Typeapplication/jsonUser-AgentVyrable-Webhook/1.0X-Vyrable-EventEvent name (e.g. content.published)X-Vyrable-Webhook-IdStable ID for this webhook subscriptionX-Vyrable-Signaturesha256=<hex> — HMAC of the raw body using your secret. Only present when a secret is set.{
"event": "content.published",
"orgId": "org_xyz",
"data": {
"contentId": "ctn_abc",
"topic": "AI search citability in 2026",
"personaId": "per_def",
"platforms": ["LINKEDIN", "X"],
"publishedAt": "2026-05-06T09:14:00Z"
},
"firedAt": "2026-05-06T09:14:01.234Z"
}Compute HMAC-SHA256 over the raw body bytes, hex-encode, and compare to the X-Vyrable-Signature header in constant time.
import { createHmac, timingSafeEqual } from "node:crypto";
// Express middleware that rejects unsigned or tampered payloads.
export function verifyVyrableWebhook(req, res, next) {
const secret = process.env.VYRABLE_WEBHOOK_SECRET;
const header = req.get("x-vyrable-signature");
if (!header?.startsWith("sha256=")) {
return res.status(401).send("missing signature");
}
const expected = createHmac("sha256", secret)
.update(req.rawBody) // raw bytes — set up express.raw() before this
.digest();
const provided = Buffer.from(header.slice(7), "hex");
if (
expected.length !== provided.length ||
!timingSafeEqual(expected, provided)
) {
return res.status(401).send("bad signature");
}
next();
}import hmac, hashlib, os
from flask import Flask, request, abort
app = Flask(__name__)
SECRET = os.environ["VYRABLE_WEBHOOK_SECRET"].encode()
@app.post("/vyrable-webhook")
def webhook():
header = request.headers.get("X-Vyrable-Signature", "")
if not header.startswith("sha256="):
abort(401)
expected = hmac.new(SECRET, request.get_data(), hashlib.sha256).hexdigest()
if not hmac.compare_digest(expected, header[7:]):
abort(401)
payload = request.get_json()
print(payload["event"], payload["data"])
return ("", 200)express.raw({ type: "application/json" }) on the route — and JSON-parsing only after the signature checks out.Every delivery is recorded in Settings → Webhooks → Deliveries with status code, response body (truncated to 2 000 chars), duration, and any error message. Failed deliveries are retried with exponential backoff. Webhooks that fail consistently are auto-disabled and you'll see a warning banner in-app — fix the receiver, click Re-enable.
Subscribe in Settings → Webhooks once you have an account. Free accounts include unlimited webhook subscriptions.