Median

POST /analytics/explore

Updated Oct 1, 20263 minute read

Explore any dataset

One query over any dataset Median collects, the way the analytics page's explorer runs it: a measure, filters, a split into series and an axis. Datasets: conversations, messages, ratings, signals, usageEvents, logs, knowledgeDocs, knowledgeSuggestions, toolRequests, toolRuns, customers, tasks. Each has its own field ids; the MCP reference (median_docs with topic analytics) lists them. A query that names a field or value the dataset lacks is refused with invalid_query and a sentence saying what was wrong.

Part of Analytics.

Request body

application/json, required.

FieldTypeRequiredDescription
queryobjectYes
query.dataset"conversations" | "ratings" | "signals" | "logs" | "knowledgeDocs" | "knowledgeSuggestions" | "toolRequests" | "toolRuns" | "customers" | "tasks" | "usageEvents" | "messages"Yes
query.measureobjectYes
query.measure.op"count" | "distinct" | "sum" | "avg" | "median" | "p75" | "p90" | "p95" | "p99" | "min" | "max" | "percent"Yescount takes no field. distinct reads an id, string or enum field. percent takes no field but a where filter, and gives the share of matched rows passing it, 0 to 100. The rest read a number or duration field.
query.measure.fieldstring | nullNoThe field id the measure reads.
query.measure.whereobjectNopercent only: the condition to count the share of. Same shape as a filter.
query.filtersobject[]NoRows have to pass every filter, or any one with match: "any". Enum values are their ids, booleans are true or false, durations are milliseconds. isSet and isNotSet take no value; in and notIn take a list of up to 50.
query.filters[].fieldstringYes
query.filters[].op"is" | "isNot" | "gt" | "gte" | "lt" | "lte" | "contains" | "notContains" | "in" | "notIn" | "isSet" | "isNotSet"Yes
query.filters[].valuestring | number | boolean | string | number | boolean[]No
query.match"all" | "any"Noall (the default) lets a row through when it passes every filter, any when it passes one.
query.groupBystring | nullNoA field id to split the rows into series by. Rows with nothing in the field land in __none.
query.topintegerNoHow many groups keep their name. The rest fold into __other, which takes one of the places.
query.order"natural" | "largest" | "smallest"Nonatural keeps the field's own order (most rows first for free text). largest or smallest ranks the groups by the measure, and the ranking picks which keep their name.
query.otherbooleanNofalse leaves the groups past top off instead of folding them into __other. The total then covers only the groups returned.
query.bucket"day" | "week" | "month" | "hour" | "weekday"NoThe x axis. day, week and month cut the window up and need the range scope; hour and weekday fold every row onto one clock.
query.whenobjectNo
query.when.fieldstringNoWhich of the dataset's time fields places a row. The dataset's first by default.
query.when.scope"range" | "all"Norange for rows inside the window, all for every row on file. Default range.
query.comparebooleanNoAlso measure the same number of days just before the window, returned as previous. Ignored with the all scope.
daysintegerNoHow far back the window reaches, counted from the UTC midnight before now. Default 30.
fromintegerNoThe window's start, unix ms, in place of counting back.

Responses

StatusDescription
200One row of values per group, one column per bucket.
400The request is malformed, and the message names the field.
401The bearer token is missing, revoked, or expired.
429Too many requests. Wait the seconds in Retry-After. Limits depend on the plan. See rate limits.

200 body

FieldTypeRequiredDescription
bucketsinteger[]NoWhen each column begins, unix ms, for a time axis. Null for a clock axis (24 hours, or Monday to Sunday) and when there is no axis.
groupsstring[]NoGroup keys in the values' order: the field's values, __none, __other, or __all for an unsplit query.
valuesnumber | null[][]NoOne list per group, one entry per column. Null where nothing measured.
totalnumber | nullNoThe measure over every matched row at once.
matchedintegerNo
scannedintegerNo
cappedbooleanNoTrue when the scan stopped at the table's cap and older rows were not read.
previousobjectNoOnly when the query set compare: the same measure over the days just before the window.
previous.fromintegerNoWhere the earlier stretch starts, unix ms. It ends where the window begins.
previous.valuesnumber | null[]NoOne value per group, in groups order, over the whole earlier stretch.
previous.totalnumber | nullNo
previous.matchedintegerNo
previous.cappedbooleanNo

Example request

curl -X POST https://api.median.sh/v1/analytics/explore \
  -H "Authorization: Bearer $BEARER_AUTH" \
  -H "content-type: application/json" \
  -d '{"query":{"dataset":"conversations","measure":{"op":"count"},"filters":[{"field":"handedOff","op":"is","value":true}],"groupBy":"channel","bucket":"week"},"days":30}'

Still need help?

    Esc