Skip to content

Read a conformed metric

GET
/v1/metrics/{name}
curl --request GET \
--url 'https://api.aerandil.com/v1/metrics/example?dimensions=channel' \
--header 'Authorization: Bearer <token>'

Read a registered conformed metric — a ratio, mean, or sum of summed measures (ADR, RevPAR, cancellation rate, forecast bias) — aggregated over its rollup table and sliced by query-time dimensions (analytics decision doc 03 §5b).

Per-metric URL. The metric name is the path segment, not a ?name= parameter. An unregistered or unparseable name is 404 (the metric URL does not exist), distinct from a 422 for a request the metric cannot satisfy.

Dimensions and filters. dimensions is a comma-separated group-by over the categorical axes the metric declares; filters[<dim>]=<value> adds equality predicates over the metric’s valid_filters subset. An axis or filter the metric does not declare is 422.

Tenant-scoped. The compiled query carries an explicit tenant predicate (RLS is defence-in-depth, not the only guard); a metric never reads across tenant boundaries.

Reads the rollup, never a live join. The answer comes from the projection-maintained rollup; the query never falls through to a join over objects / links. A rollup that is not fresh enough is a structured 503, never a slow correct answer.

Freshness gate (§5b.3, three tiers). The owning projection’s consumer lag and worker liveness combine with the rolled-up rows’ wall-clock age:

  • fresh → 200, meta.stale = false;
  • soft-stale (lag past soft_lag_seq or rows older than the metric’s soft-staleness window) → 200 with meta.stale = true — the caller may serve the number with a freshness caveat;
  • hard-stale (lag at/over hard_lag_seq, rows past the hard-staleness window, or the projection’s latest tick errored / it has never ticked) → 503 + Retry-After. The 503 body’s parameters carry required_rollup, checkpoint_lag_seq, row_freshness_seconds, and worker_unhealthy for operator triage.

Retry-After here means “retry this same request once the projection catches up” — distinct from a 412 “re-read with a fresh ETag”. Live data: always Cache-Control: no-store.

name
required
string

Registered metric name, e.g. cancellation_rate. Unknown names are 404.

dimensions
string

Comma-separated categorical group-by axes the metric declares, e.g. dimensions=property,channel. Omitted → a single rolled-up row. An undeclared axis is 422.

Example
channel
filters

Per-dimension equality filters, rendered filters[<dim>]=<value> (e.g. filters[channel]=ota). Only the metric’s valid_filters dimensions are accepted; any other key is 422.

object
key
additional properties
string

The aggregated metric. Covers both the fresh tier and the soft-stale tier — read meta.stale to tell them apart; meta also carries the lag and staleness inputs behind the decision.

Media typeapplication/json
object
dimensions
required

The categorical group-by axes echoed from the request, in request order. Each appears as a key on every row.

Array<string>
grain
required

The metric’s native temporal grain.

string
Allowed values: day week month quarter year
meta
required
object
checkpoint_lag_seq
required

The owning projection’s consumer lag in event-sequence space at read time.

integer
computed_at

The freshest computed_at across the returned rows; null for an empty result.

string | null format: date-time
hard_lag_seq
required

Consumer-lag threshold at/over which the read fails 503.

integer
hard_staleness_seconds
required

Row-age threshold past which the read fails 503.

integer
row_freshness_seconds

Wall-clock age in seconds of the freshest returned row; null for an empty result.

integer | null
soft_lag_seq
required

Consumer-lag threshold past which the read is soft-stale.

integer
soft_staleness_seconds
required

Row-age threshold past which the read is soft-stale.

integer
stale
required

true on the soft-stale tier (the answer is served but the rollup has lagged past soft_lag_seq or the rows are older than the soft-staleness window). Hard staleness is a 503, never a stale: true 200.

boolean
metric
required

The metric name (echoes the path segment).

string
rows
required
Array<object>

One aggregated cell. Each requested dimension is a property keyed by the dimension name with a string (or null) value; value is the metric’s scalar for that slice.

object
value
required

The metric’s scalar for this slice. null when the formula is undefined for the slice (e.g. a 0/0 ratio) — never NaN and never a division error.

number | null
key
additional properties

One requested dimension’s value for this row; null for a NULL dimension key.

string | null
signed
required

Whether negative values are meaningful. A signed metric (forecast bias, today the only family) is read against zero, so quote its direction and not only its magnitude; an unsigned metric floors at zero.

boolean
unit
required

What value is, so a consumer can format it without inferring from the metric name. percent is the fraction (0.62, not 62); native_unit means the unit follows a dimension value, so the read is only meaningful per that dimension.

string
Allowed values: money ratio percent count hours native_unit
version
required

MetricDef version. A formula correction or rollup-shape change ships as a new version, never an in-place mutation — pin against it to detect a semantic shift.

integer
Example
{
"grain": "day",
"unit": "money"
}
Cache-Control
string
Allowed value: no-store

Always no-store — live data.

Token lacks the analytics:read scope.

Media typeapplication/json

Structured error wrapper. Every non-2xx response from any Aerandil endpoint serialises to this shape; the HTTP status carries the failure class (4xx/5xx), the body adds a stable code for programmatic dispatch plus a human-readable message.

object
error
required
object
code
required

Stable, programmatic error code (e.g. unauthorized, forbidden, not_found, invalid_body, invalid_url, precondition_failed, no_secondary_key). Clients dispatch on code, not on the message (which may be localised or contextualised in future versions).

string
instance_id

Request-id correlation handle echoed on the request-id response header — support teams correlate without scraping logs.

string
message
required

Human-readable explanation of the failure.

string
parameters

Per-error structured detail. The shape varies by code (e.g. forbidden_action carries {"action": "..."}; precondition_failed carries {"current_version": N}).

Examplegenerated
{
"error": {
"code": "example",
"instance_id": "example",
"message": "example",
"parameters": "example"
}
}

No metric registered under that name.

Media typeapplication/json

Structured error wrapper. Every non-2xx response from any Aerandil endpoint serialises to this shape; the HTTP status carries the failure class (4xx/5xx), the body adds a stable code for programmatic dispatch plus a human-readable message.

object
error
required
object
code
required

Stable, programmatic error code (e.g. unauthorized, forbidden, not_found, invalid_body, invalid_url, precondition_failed, no_secondary_key). Clients dispatch on code, not on the message (which may be localised or contextualised in future versions).

string
instance_id

Request-id correlation handle echoed on the request-id response header — support teams correlate without scraping logs.

string
message
required

Human-readable explanation of the failure.

string
parameters

Per-error structured detail. The shape varies by code (e.g. forbidden_action carries {"action": "..."}; precondition_failed carries {"current_version": N}).

Examplegenerated
{
"error": {
"code": "example",
"instance_id": "example",
"message": "example",
"parameters": "example"
}
}

Validation error — a requested dimension or filter the metric does not declare, or a malformed filter key.

Media typeapplication/json

Structured error wrapper. Every non-2xx response from any Aerandil endpoint serialises to this shape; the HTTP status carries the failure class (4xx/5xx), the body adds a stable code for programmatic dispatch plus a human-readable message.

object
error
required
object
code
required

Stable, programmatic error code (e.g. unauthorized, forbidden, not_found, invalid_body, invalid_url, precondition_failed, no_secondary_key). Clients dispatch on code, not on the message (which may be localised or contextualised in future versions).

string
instance_id

Request-id correlation handle echoed on the request-id response header — support teams correlate without scraping logs.

string
message
required

Human-readable explanation of the failure.

string
parameters

Per-error structured detail. The shape varies by code (e.g. forbidden_action carries {"action": "..."}; precondition_failed carries {"current_version": N}).

Examplegenerated
{
"error": {
"code": "example",
"instance_id": "example",
"message": "example",
"parameters": "example"
}
}

The metric’s rollup is hard-stale — the owning projection has lagged past hard_lag_seq, the rows are older than the hard-staleness window, or the projection is unhealthy. Retry after the projection catches up.

Media typeapplication/json

Structured error wrapper. Every non-2xx response from any Aerandil endpoint serialises to this shape; the HTTP status carries the failure class (4xx/5xx), the body adds a stable code for programmatic dispatch plus a human-readable message.

object
error
required
object
code
required

Stable, programmatic error code (e.g. unauthorized, forbidden, not_found, invalid_body, invalid_url, precondition_failed, no_secondary_key). Clients dispatch on code, not on the message (which may be localised or contextualised in future versions).

string
instance_id

Request-id correlation handle echoed on the request-id response header — support teams correlate without scraping logs.

string
message
required

Human-readable explanation of the failure.

string
parameters

Per-error structured detail. The shape varies by code (e.g. forbidden_action carries {"action": "..."}; precondition_failed carries {"current_version": N}).

Examplegenerated
{
"error": {
"code": "example",
"instance_id": "example",
"message": "example",
"parameters": "example"
}
}
Retry-After
integer

Seconds to wait before retrying the same request (RFC 9110 §10.2.3).