Webhooks

Webhooks

Webhooks let you receive events as an HTTP callback instead of polling. Each delivery is a signed JSON POST you verify with a shared secret.

Registering an endpoint

Add a webhook endpoint in the Console under Settings → Notifications. The URL must be a public HTTPS endpoint; private and loopback addresses are rejected.

When you register it, you get a signing secret (a whsec_… value), shown once. Store it securely: it’s the key you verify every delivery with, and it can’t be retrieved again.

Events you can receive

actionWhen it fires
analysis.completedA file finished analyzing and its results are ready.
analysis.failedA file failed to analyze.
search.completedAn investigation (Quick Search or Deep Research) finished.
search.failedAn investigation failed.
apikey.createdAn API key was created on your account.
apikey.revokedAn API key was revoked.

Subscribe to individual actions, to a whole category (analysis, search, security), or to everything.

The delivery payload

Each event is delivered as a JSON POST body:

{
  "action": "analysis.completed",
  "label": "Analysis completed",
  "severity": "info",
  "outcome": "success",
  "resource_type": "session",
  "resource_id": "a1b2c3d4-...",
  "correlation_id": "a1b2c3d4-...",
  "activity_id": 84213
}

resource_type, resource_id, and correlation_id are optional and omitted when not applicable. activity_id is always present.

Headers

Every delivery carries two headers:

HeaderValue
X-Logcat-Signaturesha256=<hex>, the HMAC-SHA256 of the raw request body, keyed by your endpoint secret.
X-Logcat-DeliveryThe activity_id; use it as an idempotency key.

Verifying the signature

Compute HMAC-SHA256 over the exact raw bytes of the request body using your whsec_… secret, hex-encode it, prefix sha256=, and compare against X-Logcat-Signature using a constant-time comparison. Verify on the raw body before JSON parsing; re-serializing changes the bytes and breaks the signature.

Node

const crypto = require("crypto");

// Mount with the RAW body, e.g. express.raw({ type: "application/json" })
function verify(rawBody, signatureHeader, secret) {
  const expected =
    "sha256=" +
    crypto.createHmac("sha256", secret).update(rawBody).digest("hex");
  const a = Buffer.from(expected);
  const b = Buffer.from(signatureHeader || "");
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

app.post("/hooks/logcat", express.raw({ type: "application/json" }), (req, res) => {
  if (!verify(req.body, req.get("X-Logcat-Signature"), process.env.LOGCAT_WEBHOOK_SECRET)) {
    return res.status(401).send("bad signature");
  }
  const deliveryId = req.get("X-Logcat-Delivery"); // dedupe on this
  const event = JSON.parse(req.body.toString("utf8"));
  // ... handle event, idempotently keyed on deliveryId ...
  res.sendStatus(200);
});

Python

import hashlib
import hmac

def verify(raw_body: bytes, signature_header: str, secret: str) -> bool:
    expected = "sha256=" + hmac.new(
        secret.encode(), raw_body, hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(expected, signature_header or "")

# Flask example
from flask import Flask, request, abort

app = Flask(__name__)

@app.post("/hooks/logcat")
def hook():
    raw = request.get_data()  # raw bytes, before any parsing
    if not verify(raw, request.headers.get("X-Logcat-Signature"), WEBHOOK_SECRET):
        abort(401)
    delivery_id = request.headers.get("X-Logcat-Delivery")  # dedupe on this
    event = request.get_json()
    # ... handle event, idempotently keyed on delivery_id ...
    return "", 200

Retries and idempotency

A delivery is retried with backoff until your endpoint returns a 2xx, so you may receive the same event more than once. Deduplicate on X-Logcat-Delivery (the activity_id) so a retry doesn’t double-process. Return a 2xx promptly to acknowledge.

Next steps