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.
analytics_query.execution.status, execution.result, and table_preview when available.Endpoints
| Method | Path | Purpose |
|---|---|---|
GET | /api/20261031/openapi.json | Get the 20261031 OpenAPI document |
GET | /api/20261031/catalog | Get the normalized analytics catalog |
GET | /api/20261031/clients | List token-scoped reporting accounts |
GET | /api/20261031/clients/{client_id}/freshness | Get client data freshness |
POST | /api/20261031/catalog/search | Search metrics, dimensions, clients, and indexed values |
POST | /api/20261031/questions/compile | Compile and validate a natural-language analytics question |
POST | /api/20261031/questions/enqueue | Compile, validate, and enqueue an analytics question |
POST | /api/20261031/questions/run | Compile, validate, run, and fetch an analytics result |
POST | /api/20261031/queries/run | Run a structured query asynchronously or interactively (preview) |
POST | /api/20261031/queries/explain | Explain 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 parameter | Notes |
|---|---|
q | Optional substring filter across client ID, display name, external name, parent/child IDs, and stack name. |
limit | Defaults to 100 and is capped at 500. |
offset | Pagination 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
| Field | Notes |
|---|---|
question | Required for /api/20261031/questions/*. Ask for one atomic analytics query. |
client_id, client_ids | Optional but recommended. |
date_context | Optional YYYY-MM-DD base date for relative phrases. |
metric_ids, dimension_ids, sort_id, sort_direction | Exact catalog overrides. |
attribution_model, attribution_window, metric_params | Optional attribution parameters validated against the live catalog. |
limit | Optional row limit. |
format, result_format | records, rows, columnar, csv, txt, xlsx, dataframe, or arrow. |
dimension_filters | Exact indexed dimension values to constrain the query. |
wait_for_result | Whether to wait for queued completion; run endpoints wait by default. |
timeout_ms, poll_interval_ms | Optional queued wait and polling controls in milliseconds. |
response_detail | compact or full response detail. |
Structured Analytics Query Run Fields
| Field | Type and limits | Meaning |
|---|---|---|
analytics_query (required) | object | Structured query with select, filters, shape and options. Use catalog IDs for metrics and dimensions. |
query_source (required) | stringcompiled_question user_provided_query | Use user_provided_query for application-built queries, or compiled_question for compiler output. |
format | stringrecords rows columnar csv txt xlsx dataframe arrow json text | Output format alias. result_format takes precedence when both are supplied. |
result_format | stringrecords rows columnar csv txt xlsx dataframe arrow json text | Preferred output format override. |
wait_for_result | boolean Default: true | Wait for a queued result. False returns after submission. This does not select interactive execution. |
timeout_ms | integer 1000–900000 Default: 120000 | Maximum wait in milliseconds. Interactive requests, where supported, have a maximum of 120000. |
poll_interval_ms | integer 1000–30000 Default: 1000 | Interval in milliseconds between queued status checks. Not accepted for interactive requests. |
response_detail | stringcompact full | Response detail level. Does not change the requested report data. |
interactive | boolean 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 class | Operations | AQL operators |
|---|---|---|
| Numeric | equals, comparisons, between, in/not in, exists/not exists | $eq, $ne, $gt, $gte, $lt, $lte, $in, $nin, $exists, $notexists |
| Date | on, before/after, between, in/not in, exists/not exists | $eq, $ne, $gt, $gte, $lt, $lte, $in, $nin, $exists, $notexists |
| Text | equals, in/not in, contains/not contains, regex/not regex, exists/not exists | $eq, $ne, $in, $nin, $regex, $notregex, $exists, $notexists |
| Identifier | equals, in/not in, exists/not exists | $eq, $ne, $in, $nin, $exists, $notexists |
| Boolean | is/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
| Format | Notes |
|---|---|
records | JSON-style row objects. |
rows | Row-oriented output. |
columnar | Column-oriented output. |
csv | Comma-separated text payload. |
txt | Text payload. |
xlsx | Spreadsheet payload. |
dataframe | DataFrame-oriented payload for connector workflows. |
arrow | Arrow binary payload. This is the Query Builder default; responses may include execution.result.table_preview for UI inspection. |
json | Alias for records. |
text | Alias 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.
| Value | Meaning |
|---|---|
higher_is_better | A larger primary value is favorable. |
lower_is_better | A smaller primary value is favorable. |
neutral | Do not assign favorable or unfavorable coloring. |
unknown | No 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 see | What it means | What 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 / code | Meaning and action |
|---|---|
400 | Invalid fields, unsupported forecast or polling options, or unbounded client scope. Correct the request. |
403 | Client authorization or execution-mode policy denies the request. |
413 result_too_large | Reduce page size and retry. The report is not silently truncated. |
429 interactive_capacity_exceeded | Interactive capacity is occupied. Honor integer-seconds Retry-After. |
429 interactive_rate_limit_exceeded | An applicable request rate was exceeded. Honor Retry-After. |
429 monthly_execution_quota_exceeded | The shared weighted execution budget is exhausted. |
503 | Interactive execution is unavailable. An execution accepted before the failure remains charged. |
504 interactive_deadline_exceeded | The 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.
