REST API
API: Metrics
Governed metrics: definitions, values, and series. All endpoints require authentication unless noted. 18 endpoints.
GET /api/metrics
Section titled “GET /api/metrics”List governed metrics
Each metric with its owner’s and certifier’s names, and saved_query_name, the name of the saved query a metric reads, for a caller who may read saved queries (null otherwise, and for a metric on a data model). Needs the Metrics view permission. A request with any of q, certified, domain, ownerId, modelId, savedQueryId, sort, cursor or limit answers a page, \{ items, total, nextCursor }, searched, filtered and sorted on the server; one with none answers every live metric in an array by name, as before (for one release).
Query parameters
| Name | Type | Required | Description |
|---|---|---|---|
q |
string | no | Metrics whose name or description holds this, whatever its case. |
certified |
boolean | no | Only certified metrics (true), or only the others (false). |
domain |
string | no | Only metrics of this business domain, whatever its case. |
ownerId |
string | no | Only metrics this person owns. |
modelId |
string | no | Only metrics on this data model. |
savedQueryId |
string | no | Only metrics on this saved query: with certified=true, the certified metrics an edit of it changes (D3), for the editor to ask first. |
sort |
"name" | "updated" | "certified" |
no | name (the default), updated (newest change first) or certified (certified first, then by name). |
cursor |
string | no | The previous page’s nextCursor. |
limit |
integer | no | How many on a page: 50 unless given, at most 200. |
Responses: 200 · 400 · 401
POST /api/metrics
Section titled “POST /api/metrics”Create a governed metric
It reads a data model of the workspace (modelId) or a saved query of it the caller may read (savedQueryId), never both (400); another workspace’s model is not found (404). Its name is kept trimmed, one space between words: one the workspace already has, whatever its case, is a 409. Its owner is its creator unless ownerId names another active member, or is null for none; its domain is kept in the spelling the workspace uses.
Request body (JSON)
| Field | Type | Required | Description |
|---|---|---|---|
name |
string | yes | |
description |
string | no | |
modelId |
string | no | |
savedQueryId |
string | no | |
ownerId |
string | null | no | The metric’s owner, an active member of the workspace (else 400, details.reason owner_not_a_member). On create, the creator unless given; null for none (on PUT, null clears it). |
domain |
string | null | no | Its business domain, kept in the spelling the workspace already uses (Finance and finance are one); null or blank clears it. |
expression |
string | yes | |
timeDimension |
string | null | no | The field the metric’s periods are read on; null clears it. |
grain |
"day" | "week" | "month" | "quarter" | "year" |
no | |
filters |
array of — | no | |
target |
number | null | no | The metric’s goal; null for none (on PUT, null clears it). |
thresholds |
object | null | no | Warning and critical levels, such as {warn, crit}; null for none (on PUT, null clears them). |
format |
object | null | no | How its value is shown, such as {prefix, suffix, decimals}; null for the default, {} (on PUT, null resets it). |
polarity |
"higher_is_better" | "lower_is_better" | null |
no | |
additivity |
"sum" | "last" | "average" | "none" | null |
no | How the metric’s values over time make a longer period’s: sum (revenue, orders: the period’s rows add up), last (customers, balances, pipeline: its value on the period’s last day that has data, never its days added together, so its data should hold one value a day or less often), average (headcount: the mean of its values at its grain over the period), none (unique visitors: computed afresh over each period’s rows, never added across periods). last and average need a time dimension. null: not declared, evaluated as before. |
ratio |
object | null | no | A ratio’s parts, each an aggregate expression (SquareX for a model-bound metric; one aggregate over a column of the query, such as SUM(revenue), for a saved-query metric). The ratio is recomputed from its summed parts at every grain, never averaged. null: not a declared ratio. |
settlementLagDays |
integer | null | no | Days after a period ends during which its figures may still change (late invoices, refunds); until they pass the period is provisional. |
closeDay |
integer | null | no | The day of the following month on which a month’s books close (the month’s last day when it is shorter); until then a period ending in that month is provisional. |
Responses: 201 · 400 · 401 · 404 · 409
GET /api/metrics/{id}
Section titled “GET /api/metrics/{id}”Get a governed metric
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
id |
string | yes |
Responses: 200 · 400 · 401 · 404
PUT /api/metrics/{id}
Section titled “PUT /api/metrics/{id}”Update a governed metric
An edit that changes what the metric computes (its model or saved query, expression, time dimension, grain, filters, additivity, ratio parts, settlement lag or close day) makes its next version, in the same transaction, and clears its certification, which an admin grants again; the audit log records why. Other edits (name, description, owner, domain, target, thresholds, format, polarity) make no version. A certified metric is changed only by its owner or a workspace admin (403 otherwise). A field left out keeps its value; ownerId, domain, target, thresholds, format, timeDimension, polarity, additivity, ratio, settlementLagDays and closeDay clear on null. Sending modelId or savedQueryId re-points the metric to it (never both: 400), and its expression and ratio parts are checked against what it now reads unless the same change sends new ones. GET /api/metrics/{id}/versions lists the versions.
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
id |
string | yes |
Request body (JSON)
| Field | Type | Required | Description |
|---|---|---|---|
name |
string | no | |
description |
string | no | |
modelId |
string | no | |
savedQueryId |
string | no | |
ownerId |
string | null | no | The metric’s owner, an active member of the workspace (else 400, details.reason owner_not_a_member). On create, the creator unless given; null for none (on PUT, null clears it). |
domain |
string | null | no | Its business domain, kept in the spelling the workspace already uses (Finance and finance are one); null or blank clears it. |
expression |
string | no | |
timeDimension |
string | null | no | The field the metric’s periods are read on; null clears it. |
grain |
"day" | "week" | "month" | "quarter" | "year" |
no | |
filters |
array of — | no | |
target |
number | null | no | The metric’s goal; null for none (on PUT, null clears it). |
thresholds |
object | null | no | Warning and critical levels, such as {warn, crit}; null for none (on PUT, null clears them). |
format |
object | null | no | How its value is shown, such as {prefix, suffix, decimals}; null for the default, {} (on PUT, null resets it). |
polarity |
"higher_is_better" | "lower_is_better" | null |
no | |
additivity |
"sum" | "last" | "average" | "none" | null |
no | How the metric’s values over time make a longer period’s: sum (revenue, orders: the period’s rows add up), last (customers, balances, pipeline: its value on the period’s last day that has data, never its days added together, so its data should hold one value a day or less often), average (headcount: the mean of its values at its grain over the period), none (unique visitors: computed afresh over each period’s rows, never added across periods). last and average need a time dimension. null: not declared, evaluated as before. |
ratio |
object | null | no | A ratio’s parts, each an aggregate expression (SquareX for a model-bound metric; one aggregate over a column of the query, such as SUM(revenue), for a saved-query metric). The ratio is recomputed from its summed parts at every grain, never averaged. null: not a declared ratio. |
settlementLagDays |
integer | null | no | Days after a period ends during which its figures may still change (late invoices, refunds); until they pass the period is provisional. |
closeDay |
integer | null | no | The day of the following month on which a month’s books close (the month’s last day when it is shorter); until then a period ending in that month is provisional. |
Responses: 200 · 400 · 401 · 403 · 404 · 409
DELETE /api/metrics/{id}
Section titled “DELETE /api/metrics/{id}”Delete a governed metric
Refused (409 METRIC_IN_USE, details.uses) while anything uses the metric: a dashboard card, a report, a story, a deck or a template, an alert, a digest, a verified answer, an activation or a glossary term, each named where the caller may open it, else counted. force=true with the metric’s name typed (confirmName, in the body or the query; 400 when it does not match) deletes it anyway: its alerts and activations are turned off with the reason, its digests no longer list it, and its cards say it was deleted. It is restored within the workspace’s recovery window (30 days unless an admin changed it): POST /api/metrics/{id}/restore, or Recovery. The audit row keeps what it was, and which alerts, activations and digests the delete changed. A certified metric is deleted only by its owner or a workspace admin (403 otherwise, D7).
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
id |
string | yes |
Query parameters
| Name | Type | Required | Description |
|---|---|---|---|
force |
boolean | no | Delete it although something uses it, with its name typed (confirmName). |
confirmName |
string | no | The metric’s name, as typed, for a client that sends no body. |
Responses: 204 — No content · 400 · 401 · 403 · 404 · 409
GET /api/metrics/{id}/analysis
Section titled “GET /api/metrics/{id}/analysis”Trend, anomaly scan or forecast over a governed metric’s series
Runs the metric as a time series through the governed query pipeline (RLS and the metric’s filters apply) and computes a trend, the anomaly sweep’s verdict per bucket, or a forecast. from/to (both) bound the series; otherwise window (default ytd). A series too short for the analysis returns note (anomaly) or error (trend, forecast) instead of numbers.
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
id |
string | yes |
Query parameters
| Name | Type | Required | Description |
|---|---|---|---|
analysis |
"trend" | "anomaly" | "forecast" |
yes | |
window |
"30d" | "90d" | "ytd" |
no | |
from |
string | no | |
to |
string | no | |
grain |
"day" | "week" | "month" | "quarter" | "year" |
no | |
periodsAhead |
integer | no |
Responses: 200 · 400 · 401 · 404
POST /api/metrics/{id}/restore
Section titled “POST /api/metrics/{id}/restore”Restore a deleted governed metric
Within the workspace’s recovery window (30 days unless an admin changed it), as Recovery restores it: its definition, versions, owner and certification come back, and its cards, decks and stories read it again. What it reads is read again: when its saved query, a measure or a field under it changed while it was deleted, it comes back as its next version (dependency_changed), uncertified, and its owner is told. The alerts and activations its forced delete turned off stay off for their owners to turn on, and the digests it was taken out of do not list it again: restored says so and names them. 409 when a live metric has its name (rename that one first), when what it read is gone, or outside the window; 404 when the workspace has no such deleted metric.
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
id |
string | yes |
Responses: 200 · 400 · 401 · 404 · 409
POST /api/metrics/{id}/value
Section titled “POST /api/metrics/{id}/value”Evaluate a governed metric: its headline and latest whole period, all of its data and an optional series, for a period and a version, and how complete it is
include says what to compute, and only that is read: headline, allTime (value), latestPeriod, series (with series) and comparison (with period and comparison); a request without it gets today’s answer for one release (value, latestPeriod, the series and comparison it asks for) with headline beside it. headline is the metric’s number, the one to show and to read anywhere (decision D1): basis latest_period, its latest whole period at its grain that has settled, in the workspace’s time zone, with its period (grain, from, to, zone), value, previous and percent; or basis all_data, all of its data, for a metric without a time dimension and only for one; with provisional, provisionalReasons and dataThrough. Until a metric’s first whole period has settled it is named provisional (data_incomplete), and with no whole period of data the latest whole period of its grain is named with no value. value is the metric over all of its data under its filters and the request’s filters. latestPeriod is the headline’s period as the engine reads it: the metric’s latest whole period at its own grain that has data, one that ended before today (in the workspace’s time zone, Admin → Settings, UTC when none is set; the period in progress and rows dated after today never count), once the period after it has data (the source’s, whatever the date bounds the metric or the request puts on its time dimension) or has ended. Until then the latest earlier whole period that has data is sent: a part-loaded period would show a drop that is not there. Its fields: grain, the metric’s grain; start and end, the period’s first and last days (YYYY-MM-DD, end inclusive; start is the query engine’s own bucket, so a week starts on the engine’s week start); value, the metric over the period, in the same representation as the top-level value; previous, the period immediately before, always named (start, end, value), whose value is null when it has no data; and percent, the change, (value - previous.value) / |previous.value| * 100, null when either value is null or not a number, or previous.value is 0. latestPeriod is null when the metric has no time dimension, when no whole period with data has settled, or when the engine’s periods cannot be read (the server logs why): headline then says what to show (all of the data, or a period with no value yet); a request without include keeps its value when the periods cannot be read, and leaves headline out. series adds the metric bucketed over a window ending on the workspace’s today, for a trend. A metric bound to a saved query answers from the caller’s own read of the same statement in the last minute, unless the source has loaded since or refresh is true. A figure: version evaluates the metric as defined at that version (GET /api/metrics/{id}/versions), and the response names the version evaluated, the currentVersion and the definitionHash (SHA-256 of the definition). period, named as a number binding names it (a range of days; one whole period of a grain, such as 2026-Q3; or a relative period: last_complete_week, last_complete_month, last_complete_quarter, last_complete_year, week_to_date, month_to_date, quarter_to_date or year_to_date) in an IANA zone spelled as the time zone database spells it (the workspace’s by default) on the Gregorian calendar, makes value the metric over that period and latestPeriod null; the response’s period says which days it is and when it ends, closes and settles. A figure counts its period’s rows through its end, or up to now (to the minute) while it runs, so rows dated after today never count (countedTo; on a date-and-time field the zone’s days are counted by the instants that start them, up to countedUntil); the source’s last load judges how complete the figure is, never which rows it counts. A last complete period is the latest whole one whose books have closed, whose settlement lag has passed and whose data is in. A request’s comparison (kind previous_period or same_period_last_year) adds the response’s comparison: the same over the earlier period, like for like: the whole period before (or the whole month, quarter or year a year before), the same days of the one before for a period to date, a week or a day 52 weeks before, as many days just before a range. While the period runs, the earlier period is cut to the same span, as many days into it and on a date-and-time field to the same time of day, and comparison.cut says so; a date field’s rows carry no time of day, so a running day is compared with all of the same day before. The value follows how the metric adds up (additivity): a declared ratio is its summed numerator over its summed denominator (parts), never an average of ratios; last is its value on the last day that has data (valueAt, that day), never its days added together, and the latest period and the series read each period’s last day the same way; average the mean of its buckets at its grain (buckets). computedAt is when the value was computed (a cached value says when it was); loadedAt when its source’s data was loaded (a Warehouse dataset’s last refresh, a staged source’s last sync, otherwise the query’s own time); dataThrough the moment the data is complete to, and dataThroughBasis how that is known (dataset_refresh, source_sync, or query_time when the source declares no watermark); a Warehouse dataset never loaded or a staged source never synced has no watermark, so dataThrough is null; sourceState whether a dataset’s or staged source’s load is fresh, overdue or failed. Every read of the figure (its value, latest period, series and comparison) that the result cache computed before the source’s last load is read again. provisional is true, with provisionalReasons, while the period has not started (not_started) or is still running (in_progress), its books are open (books_open, before the metric’s close day), it is inside the settlement lag (settling), the data is in only up to a moment before its end (data_incomplete), or the source has no watermark, so how complete the data is is unknown (completeness_unknown, never final); without a period the value is all of the metric’s data to date (to_date). A metric bound to a saved query is evaluated too, except for a caller whom row-level security restricts on its source (403, as on a story).
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
id |
string | yes |
Request body (JSON)
| Field | Type | Required | Description |
|---|---|---|---|
include |
array of "headline" | "allTime" | "latestPeriod" | "series" | "comparison" |
no | What to compute, and nothing else: headline (the metric’s number), allTime (value, all of its data), latestPeriod, series (with series) and comparison (with period and comparison). Without it, today’s answer for one release: value, latestPeriod, the series and comparison asked for, and headline. |
filters |
array of object | no | |
refresh |
boolean | no | |
cardId |
string | no | The dashboard card that shows the metric: its value is read for a caller without Metrics who may open that card’s dashboard. Without Metrics and a card, 403. |
series |
object | no | |
version |
integer | no | Evaluate the metric as defined at this version (GET /api/metrics/{id}/versions); the current one when absent. |
period |
object | no | The period value is over, as a number binding names it: kind range (from and to, YYYY-MM-DD, both days counted), grain (one whole period of grain at: 2026, 2026-Q3, 2026-09, the ISO week 2026-W43, 2026-10-24) or relative (relative: the last complete week, month, quarter or year, or one of them to date), in zone (an IANA name as the time zone database spells it; the workspace’s when absent) on the Gregorian calendar. |
comparison |
object | no | What the figure is compared with, as a number binding names it: none, previous_period or same_period_last_year (both need period). row, a saved query’s base row, is never a metric’s. |
Responses: 200 · 400 · 401 · 404
GET /api/metrics/{id}/versions
Section titled “GET /api/metrics/{id}/versions”A governed metric’s definition versions, newest first
Each version of what the metric computes (its model or saved query, expression, time dimension, grain, filters, additivity, ratio parts, settlement lag and close day), with who made it and when, what changed against the version before, whether it cleared the metric’s certification, and what it read (its saved query, or the measures and calculated fields of its model it names): a version made because what it reads changed is dependency_changed. 20 a page by default, at most 100.
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
id |
string | yes |
Query parameters
| Name | Type | Required | Description |
|---|---|---|---|
limit |
integer | no | |
before |
integer | no | Only versions older than this one (the previous page’s nextBefore). |
Responses: 200 · 400 · 401 · 404
GET /api/metrics/digests
Section titled “GET /api/metrics/digests”List metric digests
The workspace’s digests. Needs the Metrics view permission.
Responses: 200 · 400 · 401
POST /api/metrics/digests
Section titled “POST /api/metrics/digests”Create a metric digest
Needs the metrics create permission. Its first send is the next time its hour comes round in the workspace’s time zone. Its pace sentences are the rule-based ones unless aiSentences is true, which has the AI reword them on every send and uses AI credits. A digest posts to the workspace’s digest.sent webhooks (a Slack channel) only when a workspace admin made it; anyone else’s comes to its creator alone, in the app and on their phone.
Request body (JSON)
| Field | Type | Required | Description |
|---|---|---|---|
name |
string | yes | |
metricIds |
array of string | yes | Live metrics of the workspace (else 400, details.missing); an id given twice is kept once. |
schedule |
"daily" | "weekly" |
no | |
sendHour |
integer | no | The hour of the day (0-23) it is sent at, in the workspace’s time zone (Admin → Settings; UTC when none is set): daily at that hour, or weekly on the day of its first send. A new digest without one is sent at 8:00. |
isEnabled |
boolean | no | |
aiSentences |
boolean | no | true: when the digest is sent, the AI rewords each pace sentence, given only that sentence’s numbers and verdict; it uses AI credits on every send. false (the default for every digest): the rule-based sentences only, and no AI credits. API keys can’t turn it on. |
Responses: 201 · 400 · 401 · 409
PUT /api/metrics/digests/{id}
Section titled “PUT /api/metrics/digests/{id}”Update a metric digest (name, metrics, schedule, hour, on/off, AI sentences)
Its creator, or a workspace admin (else 403). A new schedule or hour moves its next send to the next time its hour comes round. An API key can’t turn its AI sentences on, and can’t change a digest whose AI sentences are on (403): every send of it uses AI credits, so it is changed in the DataSquares app.
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
id |
string | yes |
Request body (JSON)
| Field | Type | Required | Description |
|---|---|---|---|
name |
string | no | |
metricIds |
array of string | no | Live metrics of the workspace (else 400, details.missing); an id given twice is kept once. |
schedule |
"daily" | "weekly" |
no | |
sendHour |
integer | no | The hour of the day (0-23) it is sent at, in the workspace’s time zone (Admin → Settings; UTC when none is set): daily at that hour, or weekly on the day of its first send. A new digest without one is sent at 8:00. |
isEnabled |
boolean | no | |
aiSentences |
boolean | no | true: when the digest is sent, the AI rewords each pace sentence, given only that sentence’s numbers and verdict; it uses AI credits on every send. false (the default for every digest): the rule-based sentences only, and no AI credits. API keys can’t turn it on. |
Responses: 200 · 400 · 401 · 403 · 404
DELETE /api/metrics/digests/{id}
Section titled “DELETE /api/metrics/digests/{id}”Delete a metric digest
Its creator, or a workspace admin (else 403); a digest that is not the workspace’s is not found (404).
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
id |
string | yes |
Responses: 204 — No content · 401 · 403 · 404
POST /api/metrics/digests/{id}/send
Section titled “POST /api/metrics/digests/{id}/send”Evaluate a digest now, and deliver it when it is yours
Its creator, or a workspace admin; anyone else is refused. Evaluates the digest’s metrics with your data permissions and returns them. Its creator’s Send now delivers it, as a scheduled send does; anyone else’s returns the summary only and posts nothing (delivered false). A digest posts to the workspace’s digest.sent webhooks (a Slack channel) only when a workspace admin made it; anyone else’s comes to its creator alone, in the app and on their phone. When its AI sentences are on, the AI rewords them, which uses AI credits, charged as the person who sends it; an API key gets the rule-based sentences.
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
id |
string | yes |
Responses: 200 · 400 · 401 · 403 · 404
GET /api/metrics/movers
Section titled “GET /api/metrics/movers”What moved in the caller’s followed metrics (starred, alerted, certified)
Each followed metric’s latest settled period at its own grain, in the workspace’s time zone, against up to eight periods before it: z is its distance from their mean in their own standard deviations, changePercent its change on the period just before. The period in progress never counts, nor the latest whole period until the data has moved past it, as the metric’s headline reads it. Largest move first. Needs the Metrics view permission.
Query parameters
| Name | Type | Required | Description |
|---|---|---|---|
limit |
integer | no | |
window |
"30d" | "90d" |
no | How far back a daily metric is read (30 or 90 days). Every other metric is read at its own grain over enough of its periods: 13 weeks, 13 months, 8 quarters or 9 years. |
Responses: 200 · 400 · 401 · 404
GET /api/metrics/page
Section titled “GET /api/metrics/page”List governed metrics with stable cursor pagination
Query parameters
| Name | Type | Required | Description |
|---|---|---|---|
limit |
integer | no | |
cursor |
string | no |
Responses: 200
GET /api/metrics/saved-queries/{id}/columns
Section titled “GET /api/metrics/saved-queries/{id}/columns”A saved query’s output columns, for a metric defined on it
The query’s output columns and their types (the portable type names query results carry; null when the engine does not say), read as the caller by a statement that wraps the query and reads no row: the query is never run whole. A metric on a saved query aggregates one of its numeric columns, and reads its periods on a date column. Needs the metrics create permission. 404 for a query that is not one of the workspace’s; 403 when the caller may not make metrics or read saved queries, or row-level security applies to them on its source; 400 when its SQL reads a sensitivity-labelled column their role may not read. Rate-limited as raw SQL.
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
id |
string | yes |
Responses: 200 · 400 · 401 · 403 · 404
POST /api/metrics/values
Section titled “POST /api/metrics/values”Evaluate up to 50 governed metrics in one request: each one’s headline and, on request, its series
For a list or a page of metrics: include says what to compute for each, and only that is read: headline, the metric’s number, as POST /api/metrics/{id}/value sends it (its latest whole period at its grain that has settled, in the workspace’s time zone, or all of its data for a metric without a time dimension), and series (with series: its window, 30d, 90d or ytd, and optionally its grain), the metric bucketed over a window ending on the workspace’s today, sent for a metric with a time dimension. filters apply to every metric, on top of its own. The answer has one item for each id, in the order asked: its id, and its headline and series, or error, why it has no value here, in words a person reads (a metric that no longer exists is one). A series that cannot be read beside a headline that can is left out. A metric bound to a saved query answers from the caller’s own read of the same statement in the last minute, unless its source has loaded since. Needs the Metrics view permission.
Request body (JSON)
| Field | Type | Required | Description |
|---|---|---|---|
ids |
array of string | yes | The metrics to read, each once, at most 50. |
include |
array of "headline" | "series" |
yes | What to compute for each metric, and nothing else: headline (the metric’s number) and series (with series). |
filters |
array of object | no | Filters on top of each metric’s own, as the value route’s. |
series |
object | no | The series’ window and grain, when include names series. |
Responses: 200 · 400 · 401 · 403 · 404