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
action | When it fires |
|---|---|
analysis.completed | A file finished analyzing and its results are ready. |
analysis.failed | A file failed to analyze. |
search.completed | An investigation (Quick Search or Deep Research) finished. |
search.failed | An investigation failed. |
apikey.created | An API key was created on your account. |
apikey.revoked | An 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:
| Header | Value |
|---|---|
X-Logcat-Signature | sha256=<hex>, the HMAC-SHA256 of the raw request body, keyed by your endpoint secret. |
X-Logcat-Delivery | The 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
- Analysis lifecycle & polling: the states a completion webhook corresponds to.
- Response format, errors & rate limits.