SocialCrawl

Monitor Webhooks

Signed, retried webhooks that fire when a scheduled Monitor run completes, with the run result, fired alerts, and deltas. Includes payload shapes and copy-paste signature verification in Node and Python.

Monitor Webhooks

A Monitor runs a recipe on a schedule (hourly, daily, weekly, or a cron cadence). Each time a run completes, SocialCrawl POSTs a signed JSON body to the webhook URL you registered, carrying the run result, any alerts that fired, and the deltas against the previous run. Your infrastructure reacts: post to Slack, page on-call, write to a database, flip a flag.

Billing webhooks and Monitor webhooks share the exact same signing scheme, so one verification routine covers both. If you already verify Billing Webhooks, you are done.

When a webhook fires

A Monitor webhook fires once per completed run, for runs that finish with status ok or partial:

  • ok — the recipe returned full coverage. result holds the complete unified response.
  • partial — the recipe ran but some legs did not return; you were refunded for the uncovered portion. result holds what did return.

Two run outcomes never deliver a webhook:

  • failed — the recipe returned no usable data. The run is fully refunded and no webhook is sent.
  • skipped — the run was skipped because your balance could not cover it. Nothing is charged and no webhook is sent.

If you created the Monitor with suppress_webhook_unless_alert: true, deliveries are further limited to runs where at least one alert rule fired. A quiet run (no alert) sends nothing.

Payload

Every delivery is a JSON object with this shape:

{
  "monitor_id": "mon_7Qk2R9xLpV",
  "run_id": "run_9Fh1Ab3Cd7",
  "recipe": "search/everywhere",
  "status": "ok",
  "scheduled_for": "2026-07-04T09:00:00.000Z",
  "alerts_fired": [
    {
      "metric": "data.total_results",
      "op": "pct_change_gt",
      "from": 120,
      "to": 168,
      "delta": 48,
      "pct_change": 40
    }
  ],
  "result": {
    "...": "the full unified recipe response for this run"
  },
  "deltas": {
    "data.total_results": 48
  }
}
FieldTypeDescription
monitor_idstringThe Monitor that produced this run.
run_idstringThe run row. Use it to fetch the stored run via GET /v1/monitors/{monitor_id}/runs.
recipestringThe recipe the Monitor runs.
status"ok" or "partial"The run outcome (see above). failed and skipped runs never deliver.
scheduled_forstring (ISO 8601)The scheduled slot this run filled.
alerts_firedarray of alert objectsAlert rules that matched this run. Empty when nothing fired.
resultobjectThe full unified recipe response for this run.
deltasobject (string → number)Per-metric change versus the previous comparable run. Empty on the first run (no prior to diff).

Fired alert objects

Each entry in alerts_fired describes one rule that matched:

{
  "metric": "data.total_results",
  "op": "pct_change_gt",
  "from": 120,
  "to": 168,
  "delta": 48,
  "pct_change": 40
}
FieldTypeDescription
metricstringThe dot-path into the result that the rule watches.
opstringThe matched operator: gt, lt, gte, lte, abs_change_gt, pct_change_gt, or pct_change_lt.
fromnumber or nullThe previous run's value. null for absolute-threshold ops (gt, lt, gte, lte).
tonumberThe current run's value.
deltanumber or nullto - from for delta ops. null for absolute-threshold ops.
pct_changenumber or nullPercent change versus the previous value. null for absolute ops, or when the previous value was 0.

Verifying signatures

Every delivery carries an x-socialcrawl-signature header, Stripe-style:

x-socialcrawl-signature: t=1700000000,v1=<hex>

The signature is HMAC-SHA256(secret, "<t>.<rawBody>"), where <t> is the Unix timestamp (seconds) and <rawBody> is the exact bytes of the request body. Fold t into your check to enforce a replay window.

Your signing secret is a whsec_... value. Pass your own webhook_secret when you create the Monitor, or let SocialCrawl generate one. Either way the plaintext secret is returned once, in the webhook_secret field of the create response. Store it immediately: we keep only an encrypted copy and cannot show it again.

Always verify against the raw request body. Parsing and re-serializing the JSON changes the bytes and breaks the signature.

Node.js

import crypto from "node:crypto";

function verify(rawBody, header, secret) {
  const parts = Object.fromEntries(
    header.split(",").map((kv) => kv.split("=")),
  );
  const expected = crypto
    .createHmac("sha256", secret)
    .update(`${parts.t}.${rawBody}`)
    .digest("hex");
  return crypto.timingSafeEqual(
    Buffer.from(parts.v1, "hex"),
    Buffer.from(expected, "hex"),
  );
}

Python

import hashlib, hmac

def verify(raw_body: bytes, header: str, secret: str) -> bool:
    parts = dict(kv.split("=", 1) for kv in header.split(","))
    expected = hmac.new(
        secret.encode(),
        f"{parts['t']}.".encode() + raw_body,
        hashlib.sha256,
    ).hexdigest()
    return hmac.compare_digest(parts["v1"], expected)

Delivery, retries, and auto-pause

Deliveries go out through a retrying queue (up to 5 attempts with exponential backoff). Respond 2xx promptly to acknowledge; a non-2xx response or a timeout counts as a failed attempt.

A successful delivery resets the failure counter. After 10 consecutive failed deliveries, the webhook is automatically paused and stops receiving events. Re-save the webhook URL on your dashboard to reactivate it.

URL requirements, enforced when you register a webhook:

  • Must be HTTPS.
  • Must be a public host. Loopback, private, and link-local addresses are rejected.
  • Must be 2048 characters or fewer.

Handlers should be idempotent: treat retries and occasional duplicate deliveries as normal, and de-duplicate on run_id if you need exactly-once semantics.

Monitor Webhooks | SocialCrawl