Read a conformed metric
const url = 'https://api.aerandil.com/v1/metrics/example?dimensions=channel';const options = {method: 'GET', headers: {Authorization: 'Bearer <token>'}};
try { const response = await fetch(url, options); const data = await response.json(); console.log(data);} catch (error) { console.error(error);}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_seqor rows older than the metric’s soft-staleness window) →200withmeta.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. The503body’sparameterscarryrequired_rollup,checkpoint_lag_seq,row_freshness_seconds, andworker_unhealthyfor 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.
Authorizations
Section titled “Authorizations”Parameters
Section titled “Parameters”Path Parameters
Section titled “Path Parameters”Registered metric name, e.g. cancellation_rate. Unknown names are 404.
Query Parameters
Section titled “Query Parameters”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
channelPer-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
Responses
Section titled “Responses”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.
object
The categorical group-by axes echoed from the request, in request order. Each appears as a key on every row.
The metric’s native temporal grain.
object
The owning projection’s consumer lag in event-sequence space at read time.
The freshest computed_at across the returned rows; null for an empty result.
Consumer-lag threshold at/over which the read fails 503.
Row-age threshold past which the read fails 503.
Wall-clock age in seconds of the freshest returned row; null for an empty result.
Consumer-lag threshold past which the read is soft-stale.
Row-age threshold past which the read is soft-stale.
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.
The metric name (echoes the path segment).
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
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.
One requested dimension’s value for this row; null for a NULL dimension key.
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.
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.
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.
Example
{ "grain": "day", "unit": "money"}Headers
Section titled “Headers”Always no-store — live data.
Token lacks the analytics:read scope.
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
object
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).
Request-id correlation handle echoed on the request-id response header — support teams correlate without scraping logs.
Human-readable explanation of the failure.
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.
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
object
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).
Request-id correlation handle echoed on the request-id response header — support teams correlate without scraping logs.
Human-readable explanation of the failure.
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.
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
object
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).
Request-id correlation handle echoed on the request-id response header — support teams correlate without scraping logs.
Human-readable explanation of the failure.
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.
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
object
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).
Request-id correlation handle echoed on the request-id response header — support teams correlate without scraping logs.
Human-readable explanation of the failure.
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" }}Headers
Section titled “Headers”Seconds to wait before retrying the same request (RFC 9110 §10.2.3).