Documentation / Alerting / Custom webhooks

Custom webhooks

Cirrova will POST JSON-encoded events to any HTTPS endpoint you control — signed with HMAC-SHA256, retried on failure, with optional custom headers. The full integration spec is below.

Custom webhooks are the right answer when you want Cirrova events to flow into systems you control. Email and chat channels are good for getting a notification in front of a human; webhooks are good for everything else: queues, internal APIs, SIEMs, automation runners, archiving pipelines.

When to use

Reach for a custom webhook when you want to:

  • Mirror events into your own pipeline — your audit log, data warehouse, or a queue another system consumes.
  • Trigger automation — open a ticket, run a runbook, or kick off a remediation when a budget breaches.
  • Drive an internal dashboard — keep a live feed of anomalies in your existing ops tooling.
  • Bridge to a tool we don't natively support — anything with an HTTPS endpoint will accept Cirrova events.

For sending a notification to a human, prefer the dedicated Email, Microsoft Teams, or Slack channel types — they're already shaped for that audience.

Configuring the channel

Add the channel under Organisation SettingsAlertingChannelsAdd channel with type Custom webhook. The fields are:

  • Endpoint URL — the HTTPS endpoint Cirrova will POST events to. HTTP endpoints are rejected — TLS is required.
  • Signing key (optional) — a shared secret used to compute the X-Cirrova-Signature HMAC header on every delivery. Cirrova shows the value when you create or rotate it; store it in a secret manager. See Signature verification.
  • Custom headers (optional) — arbitrary key/value pairs added to every request. Useful for an authentication header your endpoint expects, a routing tag, or a tenant identifier.

Once saved, use Send test from the row menu to dispatch a sample test event and verify reachability and signature verification before wiring the channel into any rule.

Always set a signing key in production. Without one, anyone who learns your endpoint URL can fabricate events. The signing key is the only mechanism that proves a request really came from Cirrova.

Delivery

Every delivery is an HTTPS POST with a JSON body and Content-Type: application/json. The following headers are sent on every request:

  • X-Cirrova-Event-Id — unique identifier for this delivery attempt (UUID). Stable across retries of the same event.
  • X-Cirrova-Event-Type — the event type, e.g. anomalyDetected. Matches the eventType field in the body.
  • X-Cirrova-Timestamp — Unix timestamp (seconds) at the time of delivery. Used in signature verification.
  • X-Cirrova-Signature — HMAC-SHA256 signature of the payload, prefixed sha256=. See Signature verification.

Any custom headers configured on the channel are also included.

Signature verification

Each request is signed using HMAC-SHA256 with the channel's signing key. To verify a request is genuine:

  1. Read X-Cirrova-Timestamp from the headers.
  2. Concatenate the timestamp and raw request body with a . separator: {timestamp}.{body}
  3. Compute HMAC-SHA256 of that string using your signing key (UTF-8 encoded).
  4. Hex-encode the result (lowercase).
  5. Compare with the value in X-Cirrova-Signature, which is prefixed sha256=.
expected = "sha256=" + hex(HMAC-SHA256(signingKey, timestamp + "." + rawBody))
if expected != request.headers["X-Cirrova-Signature"]:
    reject request
Replay protection: reject requests where X-Cirrova-Timestamp differs from the current time by more than a few minutes. Without a window check, an attacker who captures one valid request can replay it indefinitely.

Use a constant-time comparison when comparing the expected and received signatures — a naive string comparison can leak timing information about the signature value.

Retry schedule

If your endpoint does not return a 2xx response, Cirrova retries delivery on the following schedule:

Attempt Delay after previous attempt
260 seconds
35 minutes
415 minutes
530 minutes
660 minutes
7120 minutes

After 7 total attempts with no success the delivery is permanently marked as failed and not retried further. The Delivery history view shows every attempt and its outcome.

To avoid duplicate processing, your endpoint should respond 2xx as quickly as possible and handle any work asynchronously. The same eventId is sent on every retry of the same delivery, so you can de-duplicate by recording the eventIds you've already processed and ignoring repeats.

Cirrova enforces a 30-second timeout per delivery attempt. If your endpoint does not respond within 30 seconds the attempt is treated as a failure. The response body is ignored — only the status code and elapsed time matter.

Test deliveries sent by the Send test action are not retried on failure. They're a one-shot probe to confirm reachability and signature handling.

Payload structure

All events share a common envelope:

{
  "eventType":  "<string>",
  "eventId":    "<uuid>",
  "occurredAt": "<ISO 8601 datetime>",
  "tenancyId":  "<uuid>",
  "data": { ... }
}
  • eventType — string. Identifies the event (see below).
  • eventId — UUID. Unique identifier for this event, stable across retry attempts. Use it to de-duplicate on your end.
  • occurredAt — ISO 8601 UTC timestamp when the event was raised.
  • tenancyId — UUID. The Cirrova tenancy this event relates to.
  • data — object. Event-specific payload — see below.

anomalyDetected

Fired when a new cost anomaly is detected for a resource.

{
  "eventType":  "anomalyDetected",
  "eventId":    "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "occurredAt": "2026-04-29T10:23:00.000Z",
  "tenancyId":  "a1b2c3d4-...",
  "data": {
    "anomalyId":             "7c9e6679-...",
    "resourceName":          "my-storage-account",
    "resourceType":          "Storage account",
    "resourceGroup":         "my-resource-group",
    "subscriptionName":      "Production",
    "severity":              "High",
    "baselineDailyAverage":  12.50,
    "anomalyDailyAverage":   47.30,
    "currency":              "USD",
    "detail":                "Daily cost increased from 12.50 to 47.30 USD (278% increase over the 21-day baseline average).",
    "detectedAt":            "2026-04-29T10:23:00.000Z"
  }
}
  • anomalyId — UUID. Unique identifier for the anomaly record.
  • resourceName — display name of the affected resource.
  • resourceType — human-readable resource type.
  • resourceGroup — Azure resource group containing the resource.
  • subscriptionName — name of the Azure subscription.
  • severityLow, Medium, or High.
  • baselineDailyAverage — average daily cost over the baseline window (21 days).
  • anomalyDailyAverage — average daily cost over the anomaly detection window (7 days).
  • currency — ISO 4217 currency code (e.g. USD, AUD).
  • detail — human-readable description of the anomaly.
  • detectedAt — ISO 8601 UTC timestamp when the anomaly was first detected.

anomalyResolved

Fired when a previously-detected anomaly resolves — i.e. the resource's costs return to within normal bounds.

{
  "eventType":  "anomalyResolved",
  "eventId":    "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "occurredAt": "2026-04-29T10:23:00.000Z",
  "tenancyId":  "a1b2c3d4-...",
  "data": {
    "anomalyId":        "7c9e6679-...",
    "resourceName":     "my-storage-account",
    "resourceType":     "Storage account",
    "resourceGroup":    "my-resource-group",
    "subscriptionName": "Production",
    "severity":         "High",
    "resolvedAt":       "2026-04-29T10:23:00.000Z"
  }
}
  • anomalyId — UUID of the anomaly that has resolved. Matches the anomalyId from the original anomalyDetected event.
  • resourceName, resourceType, resourceGroup, subscriptionName — as for anomalyDetected.
  • severity — severity of the anomaly at resolution time.
  • resolvedAt — ISO 8601 UTC timestamp when the anomaly was resolved.

budgetBreached

Fired when a budget crosses a configured alert threshold.

{
  "eventType":  "budgetBreached",
  "eventId":    "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "occurredAt": "2026-04-29T10:23:00.000Z",
  "tenancyId":  "a1b2c3d4-...",
  "data": {
    "budgetId":     "9b1deb4d-...",
    "budgetName":   "Production monthly budget",
    "scope":        "Tenancy",
    "threshold":    80,
    "currentSpend": 823.45,
    "budgetAmount": 1000.00,
    "currency":     "USD",
    "period":       "Monthly"
  }
}
  • budgetId — UUID. Unique identifier for the budget.
  • budgetName — display name of the budget.
  • scopeTenancy, Subscription, ResourceGroup, or TagCollection.
  • threshold — the percentage threshold that was crossed (e.g. 80 for 80%).
  • currentSpend — current spend in the billing period at the time of the alert.
  • budgetAmount — total budget amount.
  • currency — ISO 4217 currency code.
  • periodMonthly, Quarterly, or Annual.
Each threshold fires at most once per billing period. A budget with thresholds at 50%, 80%, and 100% will deliver up to three budgetBreached events per period.

snapshotFailed

Fired when a data collection run fails during post-processing.

{
  "eventType":  "snapshotFailed",
  "eventId":    "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "occurredAt": "2026-04-29T10:23:00.000Z",
  "tenancyId":  "a1b2c3d4-...",
  "data": {
    "runId":        "d290f1ee-...",
    "errorMessage": "Post-processing exceeded the 30-minute timeout and was cancelled."
  }
}
  • runId — UUID. Identifier of the failed collection run.
  • errorMessage — description of the failure.

test

Sent when you run Send test from the channel row menu. Use this to verify your endpoint is reachable and your signature verification logic is correct before you wire the channel into any rule.

{
  "eventType":   "test",
  "eventId":     "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "occurredAt":  "2026-04-29T10:23:00.000Z",
  "tenancyId":   "a1b2c3d4-...",
  "tenancyName": "My Tenancy",
  "data": {
    "message": "This is a test webhook from Cirrova."
  }
}

Test deliveries are not retried on failure.

Responding to webhooks

Your endpoint must return an HTTP 2xx status code to acknowledge receipt. Any other status code is treated as a failure and triggers the retry schedule. The response body is ignored.

Because the same eventId is sent on every retry of the same delivery, your endpoint can de-duplicate by recording the eventIds you've already processed and ignoring repeats.

Respond quickly, work asynchronously. The 30-second timeout covers slow workloads too. If your endpoint needs to do real work in response to an event, push the work onto a queue inside your endpoint, then return 202 Accepted immediately. The retry schedule is for transport failures, not for slow handlers.

IP allowlisting

If your endpoint is behind a firewall, contact your Cirrova administrator for the IP ranges used by the webhook delivery service.