REST API Reference

20261031 — Preview

Use https://api.admetrics.io/api/20261031 as this version's API base. Select the version explicitly in the URL; the API-Version response header identifies the selected contract.

Download this version's OpenAPI specification · All supported versions · Release and upgrade notes

Authentication

REST callers can authenticate with:

Authorization: Bearer <token>

Use an existing managed API key or OAuth access token. Keep credentials in server-side configuration or your application's secure credential storage. Refresh expired OAuth tokens through the grant's existing resource and scopes. Every reporting client in a query must be authorized for the caller.

REST Query Flow

Structured query execution: build or receive a trusted structured analytics query, optionally validate it, then call POST /api/20261031/queries/run. The run endpoint validates again before execution, so backend integrations should treat this as the primary REST execution call.

1Discover IDsUse Catalog Explorer or API endpoints.2Build queryUse Query Builder or application code to assemble analytics_query.
3Validate(optional) Use builder/local checks; agent flows compile via question endpoints.
4Run queryCall POST endpoint with query payload.
5Inspect resultRead execution.status, execution.result, and table_preview when available.

Endpoints

MethodPathPurpose
GET/api/20261031/openapi.jsonGet the 20261031 OpenAPI document
GET/api/20261031/catalogGet the normalized analytics catalog
GET/api/20261031/clientsList token-scoped reporting accounts
GET/api/20261031/clients/{client_id}/freshnessGet client data freshness
POST/api/20261031/catalog/searchSearch metrics, dimensions, clients, and indexed values
POST/api/20261031/questions/compileCompile and validate a natural-language analytics question
POST/api/20261031/questions/enqueueCompile, validate, and enqueue an analytics question
POST/api/20261031/questions/runCompile, validate, run, and fetch an analytics result
POST/api/20261031/queries/runRun a structured query asynchronously or interactively (preview)
POST/api/20261031/queries/explainExplain query-planning metadata

Shared endpoints include GET /health and the aggregate GET /openapi.json. The MCP interface uses POST /mcp and retains the current contract.

Question endpoints are documented for agent and MCP-style workflows. Backend REST integrations should prefer direct structured-query execution through POST /api/20261031/queries/run.

Client Listing

Use GET /api/20261031/clients when an integration only needs accessible client IDs and names. It is lighter than fetching the full catalog and is the preferred backing endpoint for searchable client pickers.

Query parameterNotes
qOptional substring filter across client ID, display name, external name, parent/child IDs, and stack name.
limitDefaults to 100 and is capped at 500.
offsetPagination offset. Continue with next_offset until it is null.

The response includes client_count, returned_count, next_offset, and clients. Each client contains id, client_id, optional display/external names, optional stack metadata, parent client ID, and child client IDs.

Data Freshness

Use GET /api/20261031/clients/{client_id}/freshness before interpreting results that cover today or yesterday, or whenever ingestion lag could affect an answer.

curl -sS "https://api.admetrics.io/api/20261031/clients/2938354/freshness" \
-H "Authorization: Bearer ${AQL_BEARER_TOKEN}"
{
  "client_id": "2938354",
  "observed_at": "2026-08-03T10:00:00Z",
  "dbt": { "last_success_at": "2026-08-03T08:00:00Z" },
  "vendor_pull": {
    "last_success_at": "2026-08-03T09:00:00Z",
    "vendors": ["google"]
  }
}

All timestamps are UTC completion times. Both checkpoints include only regular successful runs; backfills are excluded. A token-authorized client with no successful history returns null for the relevant timestamp. Missing or invalid credentials return 401, and a valid token without access to the requested client returns 403.

Common Natural-Language Request Fields

FieldNotes
questionRequired for /api/20261031/questions/*. Ask for one atomic analytics query.
client_id, client_idsOptional but recommended.
date_contextOptional YYYY-MM-DD base date for relative phrases.
metric_ids, dimension_ids, sort_id, sort_directionExact catalog overrides.
attribution_model, attribution_window, metric_paramsOptional attribution parameters validated against the live catalog.
limitOptional row limit.
format, result_formatrecords, rows, columnar, csv, txt, xlsx, dataframe, or arrow.
dimension_filtersExact indexed dimension values to constrain the query.
wait_for_resultWhether to wait for queued completion; run endpoints wait by default.
timeout_ms, poll_interval_msOptional queued wait and polling controls in milliseconds.
response_detailcompact or full response detail.

Structured Analytics Query Run Fields

FieldType and limitsMeaning
analytics_query (required)objectStructured query with select, filters, shape and options. Use catalog IDs for metrics and dimensions.
query_source (required)string
compiled_question user_provided_query
Use user_provided_query for application-built queries, or compiled_question for compiler output.
formatstring
records rows columnar csv txt xlsx dataframe arrow json text
Output format alias. result_format takes precedence when both are supplied.
result_formatstring
records rows columnar csv txt xlsx dataframe arrow json text
Preferred output format override.
wait_for_resultboolean
Default: true
Wait for a queued result. False returns after submission. This does not select interactive execution.
timeout_msinteger
1000–900000
Default: 120000
Maximum wait in milliseconds. Interactive requests, where supported, have a maximum of 120000.
poll_interval_msinteger
1000–30000
Default: 1000
Interval in milliseconds between queued status checks. Not accepted for interactive requests.
response_detailstring
compact full
Response detail level. Does not change the requested report data.
interactiveboolean
Default: false
Return the result in the same HTTP response without polling. Applies to structured, non-forecast queries only. Using wait_for_result alone retains queued execution.

Unknown top-level fields are rejected. Keep execution controls outside analytics_query. The service validates the query before execution. Read validation before assuming that a result is present.

Records request

Replace the example client ID with an authorized reporting client. Save this body as query.json:

{
  "query_source": "user_provided_query",
  "interactive": true,
  "timeout_ms": 120000,
  "result_format": "records",
  "analytics_query": {
    "select": {
      "dimensions": [
        {
          "id": "date"
        }
      ],
      "metrics": [
        {
          "id": "consumption.spend",
          "as": "spend"
        }
      ]
    },
    "filters": [
      {
        "id": "client_id",
        "filter": {
          "$in": [
            "1234567"
          ]
        }
      },
      {
        "id": "date",
        "filter": {
          "$gte": "2026-10-01",
          "$lte": "2026-10-07"
        }
      }
    ],
    "shape": {
      "limit": 100,
      "offset": 0
    },
    "options": {
      "totals": true,
      "column_infos": true,
      "column_desc": true
    }
  }
}
curl --fail-with-body --max-time 135 \
  "https://api.admetrics.io/api/20261031/queries/run" \
  -H "Authorization: Bearer ${AQL_BEARER_TOKEN}" \
  -H 'Content-Type: application/json' --data-binary @query.json

Queued execution

A structured query uses queued execution by default. With wait_for_result: true (the default), the request waits for completion up to timeout_ms. With wait_for_result: false, it returns after submission. The response's execution contains queued, and may contain status, result, timed_out, or error. A response without execution.result is not a completed report. A wait timeout does not establish that the query was cancelled. Follow the returned job links when available instead of submitting the same query again.

Interactive execution

Set the top-level interactive: true flag to return a structured, non-forecast query's result in the same HTTP response without polling. The default is false. Setting wait_for_result: true alone still selects queued execution.

An interactive query must have a top-level client_id equality or membership filter that bounds the whole query. Every reporting client must be authorized. Interactive requests reject wait_for_result: false and poll_interval_ms. Their timeout_ms must be an integer from 1,000 to 120,000, defaulting to 120,000. The deadline covers the entire request, including waiting to start execution. Set your HTTP client's timeout slightly longer.

Successful interactive responses contain execution.mode: "interactive", a server-generated execution.execution_id, and execution.result. They have no queued object or polling URLs. Queued structured-query responses in this version use mode: "async", have an execution identity, and retain the queued response fields.

Queued result downloads in this version are available for up to five minutes after completion, or a shorter cache lifetime. Expired downloads return 410; resubmitting creates another execution. Interactive controls apply only to this version's structured query endpoint. MCP, question endpoints, and fields inside analytics_query cannot select interactive execution.

Metadata-Aware Filters

Filter-only metrics and dimensions do not need to be selected as outputs. Filters constrain the query scope; additional rows are combined with AND. Use exact IDs and parameter values from the catalog.

Field classOperationsAQL operators
Numericequals, comparisons, between, in/not in, exists/not exists$eq, $ne, $gt, $gte, $lt, $lte, $in, $nin, $exists, $notexists
Dateon, before/after, between, in/not in, exists/not exists$eq, $ne, $gt, $gte, $lt, $lte, $in, $nin, $exists, $notexists
Textequals, in/not in, contains/not contains, regex/not regex, exists/not exists$eq, $ne, $in, $nin, $regex, $notregex, $exists, $notexists
Identifierequals, in/not in, exists/not exists$eq, $ne, $in, $nin, $exists, $notexists
Booleanis/is not, exists/not exists$eq, $ne, $exists, $notexists

Use a regex to match a substring; escape characters that should be interpreted literally. Indexed dimensions offer client-scoped value suggestions through /api/20261031/catalog/search, but typed values remain valid:

{
  "dimension": "campaign_name",
  "value_query": "brand",
  "client_ids": [2617401],
  "value_limit": 12
}

A query can repeat a field; AQL merges the constraints. A range and a case-insensitive contains filter serialize as:

"filters": [
  { "id": "client_id", "filter": { "$in": [2617401] } },
  { "id": "date", "filter": { "$gte": "2026-06-01", "$lte": "2026-06-30" } },
  { "id": "attributed_events.visits", "filter": { "$gte": 1000, "$lte": 5000 } },
  { "id": "campaign_name", "filter": { "$regex": "(?i).*brand.*" } }
]

Return Formats

FormatNotes
recordsJSON-style row objects.
rowsRow-oriented output.
columnarColumn-oriented output.
csvComma-separated text payload.
txtText payload.
xlsxSpreadsheet payload.
dataframeDataFrame-oriented payload for connector workflows.
arrowArrow binary payload. This is the Query Builder default; responses may include execution.result.table_preview for UI inspection.
jsonAlias for records.
textAlias for txt.

Return Format Examples

Results are returned under execution.result. JSON formats expose the serialized report in data; text formats use text, and binary formats use base64. Read content_type, format, and byte_length before decoding. The examples below show illustrative results.

records or json

{
  "format": "records",
  "content_type": "application/json",
  "byte_length": 256,
  "data": {
    "shape": { "format": "records", "offset": 0, "results": 1, "total": 7 },
    "data": [{ "date": "2026-10-01", "spend": 1284.5 }],
    "totals": { "spend": 8120.2 }
  }
}

byte_length is illustrative. The report's data contains row objects for records, row arrays for rows, or a map of column names to arrays for columnar. The report also carries shape and requested totals and column metadata.

csv or txt

date,campaign_name,spend,revenue,roas
2026-06-01,Brand Search,1284.50,8120.20,6.32

Binary results

{
  "format": "arrow",
  "content_type": "application/vnd.apache.arrow.stream",
  "byte_length": 1024,
  "base64": "<encoded binary payload>",
  "table_preview": {
    "columns": ["date", "campaign_name", "spend", "revenue", "roas"],
    "rows": [
      {
        "date": "2026-06-01",
        "campaign_name": "Brand Search",
        "spend": 1284.5,
        "revenue": 8120.2,
        "roas": 6.32
      }
    ],
    "row_count": 1,
    "truncated": false
  }
}

base64 contains the entire returned binary payload. Decode Arrow as an IPC stream and consume every record batch. table_preview is only a display sample; it must not be used as the full report.

Arrow request

{
  "query_source": "user_provided_query",
  "interactive": true,
  "timeout_ms": 120000,
  "result_format": "arrow",
  "analytics_query": {
    "select": {
      "dimensions": [
        {
          "id": "date"
        }
      ],
      "metrics": [
        {
          "id": "consumption.spend",
          "as": "spend"
        }
      ]
    },
    "filters": [
      {
        "id": "client_id",
        "filter": {
          "$in": [
            "1234567"
          ]
        }
      },
      {
        "id": "date",
        "filter": {
          "$gte": "2026-10-01",
          "$lte": "2026-10-07"
        }
      }
    ],
    "shape": {
      "limit": 100,
      "offset": 0
    },
    "options": {
      "totals": true,
      "column_infos": true,
      "column_desc": true
    }
  }
}

Totals, pagination and partial results

For JSON results, inspect the report under execution.result.data. shape.total is the authoritative row count before pagination, shape.offset is the requested offset, and shape.results is the returned count. Continue from the next offset while more rows remain. An empty page beyond the last row does not mean the complete report is empty.

Request totals with analytics_query.options.totals: true. Totals include metric filters and are calculated before pagination. Do not sum a page or average displayed ratios to reconstruct them. If a KPI deliberately excludes table metric filters, submit a separate query with the intended scope filters.

Request options.column_infos and options.column_desc independently. Column information describes output identities, aliases, parameters, formatting and scaling. Inspect shape.missing for incomplete metric evaluation; a null value alone does not prove a metric is missing or complete. Apply display multipliers once. Records may apply the requested rounding precision; Arrow retains typed numeric values. Preserve 64-bit grouping keys rather than converting them to lossy numbers.

Arrow uses a nullable 64-bit integer column when all grouping keys fit that type. If an integer-declared grouping column contains a text placeholder or an integer outside the signed 64-bit range, the whole Arrow column uses strings so those values remain exact. Missing keys remain null. Records retain numeric keys where possible, so compare these mixed columns by their values rather than requiring identical JSON and Arrow types. Use the actual Arrow field type when decoding each result.

Arrow metadata

Set result_format to "arrow" in the same request. The JSON envelope still contains execution.result.base64, content_type, format, and the decoded byte length. The base64 data is a normal Arrow IPC stream, not a new container format.

Read the stream schema's admetrics:result entry as UTF-8 JSON. Application metadata is part of the Arrow format. The entry is present even for zero-row and empty paginated results. Other Arrow metadata is preserved. This illustrative document shows the fields consumers should inspect:

{
  "metadata_version": 1,
  "execution_id": "50bca09a-e3cd-44cc-a12f-5429ff4d159c",
  "query_id": "query-identity",
  "shape": {
    "format": "arrow",
    "offset": 100,
    "results": 25,
    "total": 125,
    "column_infos": {
      "spend": {
        "id": "consumption.spend",
        "ext_as": "spend",
        "num_style": "currency",
        "multiplier": 1
      }
    }
  },
  "totals": { "spend": 1234.5 }
}

shape.total is the authoritative row count before pagination, offset is the actual requested offset, and results is the returned count. column_infos is keyed by the actual Arrow field names, including aliases and statistical output suffixes. Metric identities use public IDs. The metadata preserves output identities, parameters, formatting and scaling; it does not rename caller aliases.

Request totals with options.totals. Request column metadata with column_infos and column_desc independently. Records and Arrow describe the same result and use the same totals semantics. Totals include the query's metric filters and are calculated before pagination; do not compute them by summing a page or averaging displayed ratios. A dashboard KPI that deliberately excludes table metric filters still needs a separate query containing only its intended scope filters.

Check shape.missing for incomplete metric evaluation, including null-valued outputs. An empty list or absent field means no metrics were reported missing; a null alone does not establish completeness. Check offset + results against total before treating a page as the full report. table_preview, when supplied, is a display aid, not full data.

Decode in Python

import base64
import json
import pyarrow.ipc as ipc

# envelope is the parsed HTTP JSON response.
reader = ipc.open_stream(base64.b64decode(envelope["execution"]["result"]["base64"]))
metadata = json.loads(reader.schema.metadata[b"admetrics:result"].decode("utf-8"))
assert metadata["metadata_version"] == 1
assert metadata["execution_id"] == envelope["execution"]["execution_id"]
table = reader.read_all()  # consumes every record batch
assert table.num_rows == metadata["shape"]["results"]

Decode in JavaScript

import { tableFromIPC } from "apache-arrow";

const bytes = Buffer.from(envelope.execution.result.base64, "base64");
const table = tableFromIPC(bytes);
const text = table.schema.metadata.get("admetrics:result");
if (!text) throw new Error("Missing Admetrics result metadata");
// Modern JavaScript engines expose the original JSON number through context.source.
const metadata = JSON.parse(text, (_key, value, context) => {
  if (typeof value === "number" && Number.isInteger(value) && !Number.isSafeInteger(value)) {
    if (!context?.source) throw new Error("Use an integer-preserving JSON parser on this runtime");
    // Decimal/exponent literals remain floating-point metrics.
    if (/^-?\d+$/.test(context.source)) return BigInt(context.source);
  }
  return value;
});
if (metadata.metadata_version !== 1) throw new Error("Unsupported metadata version");
if (metadata.execution_id !== envelope.execution.execution_id) throw new Error("Execution mismatch");
if (table.numRows !== metadata.shape.results) throw new Error("Row count mismatch");

Use an integer-preserving JSON parser on runtimes without context.source. Arrow 64-bit integer columns should retain their library representation (often bigint); avoid converting grouping keys to JavaScript Number or round-tripping them through ordinary JSON. Floating-point metrics remain floating point. Formatting and multipliers describe presentation; apply a display multiplier once, and keep the underlying value. Records can apply the requested rounding precision; Arrow retains its typed numeric values.

Decode in Rust

use std::io::Cursor;
use arrow_ipc::reader::StreamReader;
use base64::{engine::general_purpose::STANDARD, Engine};
use serde_json::Value;

fn decode(envelope: &Value) -> Result<(), Box<dyn std::error::Error>> {
    let encoded = envelope["execution"]["result"]["base64"].as_str().ok_or("Missing IPC data")?;
    let bytes = STANDARD.decode(encoded)?;
    let reader = StreamReader::try_new(Cursor::new(bytes), None)?;
    let schema = reader.schema();
    let text = schema.metadata().get("admetrics:result").ok_or("Missing metadata")?;
    let metadata: Value = serde_json::from_str(text)?;
    if metadata["metadata_version"] != 1 { return Err("Unsupported metadata version".into()); }
    if metadata["execution_id"] != envelope["execution"]["execution_id"] { return Err("Execution mismatch".into()); }
    let mut rows = 0usize;
    for batch in reader { rows += batch?.num_rows(); }
    if Some(rows as u64) != metadata["shape"]["results"].as_u64() { return Err("Row count mismatch".into()); }
    Ok(())
}

Catalog and client metadata

Fetch the catalog to discover exact metric and dimension IDs and their supported parameters. Use POST /api/20261031/catalog/search to narrow results by question, or to find indexed values with dimension, value_query, and client_ids. limit bounds catalog matches; value_limit defaults to 20 (maximum 50), and dimension_limit defaults to 3 (maximum 5). Catalog visibility follows the caller's permissions.

Comparison preference

Catalog metrics include comparison_preference. It describes the primary value or point estimate, and does not automatically apply to confidence bounds or interval width.

ValueMeaning
higher_is_betterA larger primary value is favorable.
lower_is_betterA smaller primary value is favorable.
neutralDo not assign favorable or unfavorable coloring.
unknownNo supported preference is available; avoid guessing.

This field does not perform period comparisons or provide global pagination across two periods.

Client currency

Client-list items and catalog client entries include nullable currency, such as "EUR" or "USD". Unknown currency is null. This is the client's reporting default; explicit query or output currency takes precedence. It does not guarantee the currency of every monetary output.

Send concrete date bounds. A reporting date follows the source's reporting-day convention; it is not automatically a UTC timestamp or a guarantee that every source has the same timezone.

Usage and limits

A queued execution consumes one weighted execution unit. Raw request counts and execution counts are reported separately. Protected requests can count as calls even when no execution starts. Applicable account, credential and reporting-client limits all apply; a reporting client is an Admetrics account, distinct from an OAuth application's client_id.

An accepted interactive execution consumes two weighted units by default; the weight can vary by plan. Queued and interactive executions share the weighted monthly execution budget. Existing finite execution allowances supply the default weighted allowance, and unlimited plans remain unlimited. Historical executions keep their original one-unit charge.

Cache hits, execution failures, timeouts and abandoned requests count once the execution is accepted. Authentication, validation, quota and capacity rejections before acceptance consume no execution units. Each accepted retry is another execution. Four interactive queries normally consume eight units.

A multi-client query checks the limits for each distinct reporting client but incurs one execution charge. OAuth refresh does not reset quota identity. Owners can inspect effective limits; administrators manage them. Interactive capacity remains occupied until execution ends, so disconnecting does not immediately free capacity.

The default decoded-result limit is 64 MiB. Base64 adds roughly one third to a binary payload's size, and your HTTP client may impose a smaller limit. On an oversized-result error, reduce the page size and submit a new request. Reports are never silently truncated or replaced with preview rows. A new accepted submission is a new execution charge.

Troubleshooting Failed Requests

Most failures mean the request needs a more precise analytics contract. Treat these as recoverable integration states rather than generic server errors.

What you seeWhat it meansWhat to do
The response asks for clarification.AQL could not infer enough scope to build one clear query.Add an explicit client, date range, metric, and grouping. For REST integrations, prefer exact catalog IDs over labels.
Validation failed.The generated or supplied structured query contains an unsupported field, filter, parameter, or field combination.Read validation.errors, replace guessed fields with IDs from Catalog Explorer or /api/20261031/catalog/search, then retry.
The run timed out.The response deadline expired before the result was returned.Use a larger timeout_ms within the selected mode's limit, or use queued execution for longer queries.
The request returns 401.The bearer token is missing, expired, invalid or revoked.Refresh the token or authenticate again.
The request returns 403.The caller lacks access to the client or the requested execution mode.Check client permissions and the applicable policy.
The response says the output format is unsupported.The requested format or result_format is not available for this endpoint.Use one of the supported formats listed above, such as records, csv, xlsx, or arrow.
The request returns 413.The result exceeds the payload limit.Reduce page size and submit a new request.
The request returns 429.A request rate, capacity or execution-budget limit was reached.Read the error code and honor Retry-After when present. A monthly budget requires an allowance change or the next period.
The request returns 503.Execution is unavailable for this request.The selected mode may be disabled, or the service may be temporarily unavailable. An already accepted execution remains charged.

Interactive errors

Status / codeMeaning and action
400Invalid fields, unsupported forecast or polling options, or unbounded client scope. Correct the request.
403Client authorization or execution-mode policy denies the request.
413 result_too_largeReduce page size and retry. The report is not silently truncated.
429 interactive_capacity_exceededInteractive capacity is occupied. Honor integer-seconds Retry-After.
429 interactive_rate_limit_exceededAn applicable request rate was exceeded. Honor Retry-After.
429 monthly_execution_quota_exceededThe shared weighted execution budget is exhausted.
503Interactive execution is unavailable. An execution accepted before the failure remains charged.
504 interactive_deadline_exceededThe response deadline expired. Cancellation is best effort; an accepted execution remains charged.

Check the HTTP status and content type before decoding an error body. Gateways can return their own error representation. Use bounded backoff with jitter for temporary errors and honor Retry-After where provided. A timeout or disconnect does not guarantee cancellation.