Metrics & Governance
Webhooks
Webhooks enqueue outbound notifications when DataSquares events happen, so your receiver does not poll. Delivery is asynchronous and best-effort: the product action has already succeeded if enqueueing or delivery later fails. Point one at Slack for readable messages or at your own endpoint for automation.
Two webhook kinds
Section titled “Two webhook kinds”| Kind | Payload |
|---|---|
| generic | structured JSON — for your own services and automation |
| slack | a ready-to-post Slack incoming-webhook message (text + blocks, markdown sections with a compact field list) — paste a Slack webhook URL and events render readably in the channel, no middleware |
Events
Section titled “Events”Each webhook subscribes to the events it cares about. The ones you’ll use first:
| Event | Fires when |
|---|---|
alert.fired |
a metric alert crosses its threshold, when the alert’s creator administers the workspace (below) |
digest.sent |
a metric digest that a workspace admin made runs — Slack hooks get one readable line per metric, with pace status |
source.sync_failed |
a scheduled source refresh fails |
metric.anomaly_detected |
a certified metric’s scheduled anomaly sweep finds an unusual movement, when the metric’s creator administers the workspace (below) |
model.created / model.published |
a data model is created or published |
dashboard.created / dashboard.published / dashboard.deleted |
a dashboard changes lifecycle state |
report.deleted |
a report is deleted |
dashboard.restored / report.restored / story.restored / presentation.restored / metric.restored |
a deleted item is restored from the Recovery Center |
report.delivered |
a scheduled report delivery completes |
pipeline.run_failed |
a pipeline run reaches failed state |
usage.budget_exceeded |
product usage crosses its configured budget |
access.granted / access.revoked |
a resource access grant changes |
compute.queue_stuck, compute.vm_unreachable, compute.provision_failing |
compute recovery detects an operational fault — sent only to the workspaces of the people who run the machines (on a self-hosted install, your admins) |
webhook.test |
you press test — verify the pipe before trusting it |
A dashboard or report deleted by a person arrives with deletedBy. One the system removed
automatically — an untitled draft nobody changed for 7 days, see
Drafts nobody touched — arrives with
reason: "untouched_draft" and no deletedBy.
Event names are free-form subscriptions rather than a server-validated enum. Subscribing to a name that nothing emits is accepted but produces no delivery.
Alert firings
Section titled “Alert firings”An alert.fired event is the alert’s own check, read as its creator. Its number
is the creator’s, so the workspace’s channels carry it only when that person
administers the workspace, as with digests and anomalies. The alert’s email and
push tell each person in their own number instead
(Alert rules).
| Field | What it holds |
|---|---|
alertId |
the alert’s id |
name |
the alert’s name |
value |
the number it compared; null when it is withheld |
withheld |
only when the value may not leave DataSquares, why, in words: It reads a sensitivity-labelled field that can’t be shown here, so its value is withheld., or Its value is withheld: DataSquares could not check that it may be sent. when that check fails |
threshold |
its threshold |
period |
for an alert on a metric, the period it compared, as a person names it (October 2026, November 2026 to date, All data); null for one on a field or a log search |
valueText |
the value as written: in the metric’s format for an alert on a metric, grouped otherwise, or the withheld sentence |
An alert on a metric’s period is told again for each new period it compares while it stays past its threshold, so one alert can send one event per period.
Metric anomalies
Section titled “Metric anomalies”A metric.anomaly_detected event is the anomaly sweep’s finding for one metric
and one period, read as the metric’s creator. Its numbers are the creator’s, so
the workspace’s channels carry it only when that person administers the
workspace (a company admin, or a member whose role holds the workspace’s admin
rights). Anyone else’s finding is told only in the app, to the creator, the
metric’s owner and its followers, each in their own numbers. A digest’s
digest.sent follows the same rule, for the person who made the digest.
Its data, the sentences first, then the figures for automation:
| Field | What it holds |
|---|---|
summary |
the finding in words: Revenue for October 2026 is unusually low: 31% under its usual level. |
detail |
the two numbers, in the metric’s format: October 2026: 812,000, against a usual level of 1,180,000. |
name |
the metric’s name |
period |
the period judged, as a person names it (October 2026, Q3 2026, Week of 5 Oct) |
direction |
spike or drop |
severity |
medium, high or critical |
bucket |
the period’s first day (2026-10-01) |
metricId |
the metric’s id |
value |
the period’s value |
usual |
its usual level: the average of the periods before it in the sweep’s window |
zScore, driftPct |
how many standard deviations the value is from the usual level, and how far it is from the median of those periods, as a share of it (0.31 for 31%) |
A Slack hook titles the event Unusual change in a metric and shows the first eight of these fields, so the sentences lead.
A metric that reads a sensitivity-labelled field sends why in place of its
numbers, whatever its creator’s role: summary (Revenue for October 2026: It
reads a sensitivity-labelled field that can’t be shown here, so its value is
withheld.), name, period, bucket, metricId and withheld, the sentence
alone, with no detail, figures or statistics.
Managing webhooks
Section titled “Managing webhooks”Webhooks are managed through the REST API today (no web UI yet): create a webhook with its URL, kind, and event subscriptions; send a test delivery; and inspect recent deliveries to debug a silent channel.
You can update the name, URL, subscriptions, headers, secret, or enabled state,
and delete a webhook. Kind cannot be changed after creation; create a new
webhook to switch between generic and Slack formatting. Secrets are used
internally and are never returned by list/create responses. A secret is stored
encrypted; a webhook whose secret can no longer be decrypted (after the
platform’s encryption key changes without it) fails its deliveries with
The webhook’s signing secret could not be decrypted with the configured encryption key. Set the secret again.,
rather than send them unsigned. Without an encryption key on the server, setting a secret is refused (503,
The webhook’s secret cannot be stored: no encryption key is configured.). An empty secret removes it.
Who can manage webhooks
Section titled “Who can manage webhooks”A webhook sends the workspace’s events to an address outside DataSquares, so
managing them is a company admin’s job, as the workspace’s other settings are.
Listing, creating, changing, testing and deleting webhooks, and reading their
deliveries, all need a company admin signed in (with the admin settings permission, which a company admin has),
in the workspace. Anyone
else, a member or a viewer, is refused with 403, and so is an API key,
whatever its scope: call these endpoints with a signed-in admin’s session token
(Authorization: Bearer), as below. A webhook is reachable only from its own
workspace; another workspace’s id answers 404.
Members still benefit from them: a webhook delivers the events the product emits for the workspace, and anyone who can share a story can post it to one of the workspace’s Slack webhooks (by name; its address is never shown) with Share to Slack.
curl -X POST https://<your-host>/api/webhooks \ -H "Authorization: Bearer <token>" -H "Content-Type: application/json" \ -d '{ "name": "Operations Slack", "url": "https://hooks.slack.com/services/T000/B000/XXXX", "kind": "slack", "events": ["alert.fired", "source.sync_failed"] }'Generic payload and signatures
Section titled “Generic payload and signatures”Generic deliveries use this envelope:
{ "event": "alert.fired", "timestamp": "2026-08-27T10:00:00.000Z", "data": {}}Every attempt includes X-DataSquares-Timestamp and a stable
X-DataSquares-Delivery-Id for receiver-side deduplication. If a secret is
configured, X-DataSquares-Signature is
sha256=<HMAC-SHA256(secret, timestamp + "." + deliveryId + "." + rawBody)>.
Reject stale timestamps and compare signatures in constant time. The delivery
ID stays the same across retries; the signed timestamp changes per attempt.
Custom headers are merged first. DataSquares then overwrites protected headers such as content type, user agent, delivery ID, timestamp, and signature so a stored custom header cannot forge them.
Delivery, retries, and diagnostics
Section titled “Delivery, retries, and diagnostics”Each subscribed webhook gets its own delivery record, pending until it is delivered or failed. The retry
budget is five attempts: after a failed attempt the next is due 1, 2, 4, then 8 seconds later, and the fifth failure
marks it failed. Due deliveries are picked up every 30 seconds, so in practice attempts are about half a minute
apart or more. Each outbound request has a configurable
timeout (10 seconds by default).
Targets are checked when configured and again at delivery time to resist DNS rebinding. Public deployments reject private, loopback, and cloud-metadata addresses; a self-hosted operator may explicitly allow private targets. Redirects are never followed because the redirect destination has not passed the target check. A blocked target is marked permanently failed.
POST /api/webhooks/{id}/test queues a webhook.test delivery and returns the
test envelope; it does not wait for a successful HTTP response. Inspect
GET /api/webhooks/{id}/deliveries for newest-first status, attempts, last
error, retry time, and delivery time. The list defaults to 50 rows and supports
up to 200 per page with an offset.