Back to developers
Webhooks

Real-time events,
HMAC-signed.

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.

Setup

  1. Open Settings → Webhooks and click Create.
  2. Enter your receiver URL and pick the events you want.
  3. Optional but strongly recommended: set a secret. Vyrable will use it to compute an HMAC-SHA256 signature on every delivery.
  4. The receiver should respond 2xx within 10 seconds. Non-2xx responses are logged in the deliveries view and counted toward auto-disable thresholds.

Events

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

6 events
  • 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.

Ideas

2 events
  • idea.createdAn idea is captured to Mission Control (UI / API / extension).
  • idea.assignedAn idea's assignee changes (human or agent).

Campaigns

1 event
  • campaign.activatedA campaign moves from draft to active.

Reviews

2 events
  • review.approvedEditorial review approves a content row.
  • review.rejectedEditorial review rejects a content row.

Visibility

1 event
  • visibility.alertAI Visibility tracker emits an alert (score_drop / prompt_zeroed / competitor_added).

Intel

3 events
  • 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.

Delivery shape

Headers (every delivery)

  • Content-Typeapplication/json
  • User-AgentVyrable-Webhook/1.0
  • X-Vyrable-EventEvent name (e.g. content.published)
  • X-Vyrable-Webhook-IdStable ID for this webhook subscription
  • X-Vyrable-Signaturesha256=<hex> — HMAC of the raw body using your secret. Only present when a secret is set.

Sample payload

{
  "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"
}

Signature verification

Compute HMAC-SHA256 over the raw body bytes, hex-encode, and compare to the X-Vyrable-Signature header in constant time.

Node (Express)

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();
}

Python (Flask)

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)
Always verify against the raw bytes of the body, not a parsed-and-restringified copy. Re-serialising changes whitespace and breaks the signature. In Express that means express.raw({ type: "application/json" }) on the route — and JSON-parsing only after the signature checks out.

Retries + delivery log

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.

Wire your stack to publish events.

Subscribe in Settings → Webhooks once you have an account. Free accounts include unlimited webhook subscriptions.