citlyze docs
API

Webhooks

Receive alerts and scheduled reports as signed JSON on your own HTTPS endpoint.

A custom webhook sends the same alerts and scheduled reports that go to email, Slack and Microsoft Teams to an HTTPS endpoint you run, as JSON. Use it to feed an automation tool, a ticketing system or your own service.

Set it up

In Citlyze, open Workspace Settings. In Notification webhooks, connect Custom webhook, enter your endpoint URL and select Save. Citlyze shows a signing secret once: copy it and store it with your endpoint's other secrets. Send test posts a test event so you can check the connection.

The endpoint must:

  • use https:// on the standard port, with a public hostname (not an IP address);
  • answer with a 2xx status within 10 seconds. A redirect counts as a failure and is not followed.

Each workspace has one custom webhook. Pause it to stop deliveries without losing the URL; remove it and connect again to get a new signing secret.

The request

Citlyze sends a POST with a JSON body:

{
  "version": 1,
  "id": "6f1d1c0e-8a53-4c9e-9d53-0c1c5e0a7b21",
  "type": "alert",
  "created_at": "2026-11-10T12:00:00.000Z",
  "title": "Acme visibility dropped",
  "summary": ["Visibility fell from 61% to 44% over the last 7 days."],
  "metrics": [
    { "label": "Before", "value": "61%", "detail": null },
    { "label": "Now", "value": "44%", "detail": null }
  ],
  "rows": [],
  "url": "https://app.citlyze.com/dashboard"
}
FieldTypeDescription
versionnumberVersion of this format. Currently 1.
idstringUnique for each delivery.
typestringalert, report or test.
created_atstringWhen the request was made, ISO 8601 in UTC.
titlestringThe headline of the alert or report.
summarystring[]One or more sentences describing it.
metricsobject[]Headline numbers, each with label, value and an optional detail.
rowsobject[]Supporting lines, such as affected prompts or top sources, in the same shape as metrics.
urlstring or nullWhere to read more in the app.

The text fields are written for people. Show them as they are rather than parsing them: wording can change, and new fields may be added without a new version.

Verify the signature

Every request carries two headers:

X-Citlyze-Timestamp: 1794312000
X-Citlyze-Signature: sha256=3b0c...e9

The signature is an HMAC-SHA256 of the timestamp, a dot and the raw request body, keyed with your signing secret and written as lowercase hex. Compute it over the bytes you received, before parsing the JSON, and compare it in constant time. Reject a request whose timestamp is more than five minutes old.

import { createHmac, timingSafeEqual } from "node:crypto";

export function isFromCitlyze(rawBody, headers, secret) {
  const timestamp = headers["x-citlyze-timestamp"] ?? "";
  const received = headers["x-citlyze-signature"] ?? "";
  if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;
  const expected =
    "sha256=" + createHmac("sha256", secret).update(`${timestamp}.${rawBody}`).digest("hex");
  return (
    received.length === expected.length &&
    timingSafeEqual(Buffer.from(received), Buffer.from(expected))
  );
}
import hashlib, hmac, time

def is_from_citlyze(raw_body: bytes, headers, secret: str) -> bool:
    timestamp = headers.get("X-Citlyze-Timestamp", "")
    received = headers.get("X-Citlyze-Signature", "")
    if not timestamp.isdigit() or abs(time.time() - int(timestamp)) > 300:
        return False
    signed = timestamp.encode() + b"." + raw_body
    expected = "sha256=" + hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
    return hmac.compare_digest(received, expected)

Delivery

A delivery that fails is not sent again, so treat a webhook as a notification rather than a record. The alert history and your scheduled reports in the app stay the place to check what was sent. For data you want to pull on your own schedule, use the REST API.

On this page