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.
POST
/analytics/exploreAuthorization
AuthorizationBearer token · headerrequired`MEDIAN_KEY` from Settings under API, or an MCP OAuth access token. A key acts as the organization; a token acts as the person who approved it.
Request body
requiredapplication/jsonqueryobjectrequiredShow propertiesHide properties
datasetstringrequiredAllowed:
conversationsratingssignalslogsknowledgeDocsknowledgeSuggestionstoolRequeststoolRunscustomerstasksusageEventsmessagesmeasureobjectrequiredShow propertiesHide properties
opstringrequiredcount 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.
Allowed:
countdistinctsumavgmedianp75p90p95p99minmaxpercentfieldstring | nullThe field id the measure reads.
whereobjectpercent only: the condition to count the share of. Same shape as a filter.
Show propertiesHide properties
fieldstringrequiredopstringrequiredvalueanyfiltersobject[]Rows 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.
Show propertiesHide properties
Array of
objectfieldstringrequiredopstringrequiredAllowed:
isisNotgtgteltltecontainsnotContainsinnotInisSetisNotSetvaluestring | number | boolean | string | number | boolean[]Show propertiesHide properties
One of:
string
stringnumber
numberboolean
booleanstring | number | boolean[]
Array of
string | number | booleanOne of:
string
stringnumber
numberboolean
booleanmatchstringall (the default) lets a row through when it passes every filter, any when it passes one.
Allowed:
allanygroupBystring | nullA field id to split the rows into series by. Rows with nothing in the field land in `__none`.
topintegerHow many groups keep their name. The rest fold into `__other`, which takes one of the places.
min 1 · max 20 · default: 8
orderstringnatural 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.
default: "natural"
Allowed:
naturallargestsmallestotherbooleanfalse leaves the groups past `top` off instead of folding them into `__other`. The total then covers only the groups returned.
default: true
bucketstring | nullThe x axis. day, week and month cut the window up and need the range scope; hour and weekday fold every row onto one clock.
Allowed:
dayweekmonthhourweekdaywhenobjectShow propertiesHide properties
fieldstringWhich of the dataset's time fields places a row. The dataset's first by default.
scopestringrange for rows inside the window, all for every row on file. Default range.
Allowed:
rangeallcomparebooleanAlso measure the same number of days just before the window, returned as `previous`. Ignored with the all scope.
default: false
daysintegerHow far back the window reaches, counted from the UTC midnight before now. Default 30.
min 1 · max 366
fromintegerThe window's start, unix ms, in place of counting back.
Responses
200One row of values per group, one column per bucket.
bucketsinteger[] | nullWhen 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[]Group keys in the values' order: the field's values, `__none`, `__other`, or `__all` for an unsplit query.
valuesnumber[][]One list per group, one entry per column. Null where nothing measured.
totalnumber | nullThe measure over every matched row at once.
matchedintegerscannedintegercappedbooleanTrue when the scan stopped at the table's cap and older rows were not read.
previousobjectOnly when the query set `compare`: the same measure over the days just before the window.
Show propertiesHide properties
fromintegerWhere the earlier stretch starts, unix ms. It ends where the window begins.
valuesnumber[]One value per group, in `groups` order, over the whole earlier stretch.
totalnumber | nullmatchedintegercappedboolean400The request is malformed, and the message names the field.
errorobjectShow propertiesHide properties
codestringmessagestring401The bearer token is missing, revoked, or expired.
errorobjectShow propertiesHide properties
codestringmessagestring429Too many requests. Wait the seconds in `Retry-After`. Limits depend on the plan. See [rate limits](/api/errors-and-limits#rate-limits).
errorobjectShow propertiesHide properties
codestringmessagestringRequest
curl -X POST "https://api.median.sh/v1/analytics/explore" \
-H "Authorization: Bearer YOUR_TOKEN" \
-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
}'const response = await fetch("https://api.median.sh/v1/analytics/explore", {
method: "POST",
headers: {
"Authorization": "Bearer YOUR_TOKEN",
"Content-Type": "application/json"
},
body: JSON.stringify({
"query": {
"dataset": "conversations",
"measure": {
"op": "count"
},
"filters": [
{
"field": "handedOff",
"op": "is",
"value": true
}
],
"groupBy": "channel",
"bucket": "week"
},
"days": 30
})
});Response
{
"buckets": [
0
],
"groups": [
"string"
],
"values": [
[
0
]
],
"total": 0,
"matched": 0,
"scanned": 0,
"capped": true,
"previous": {
"from": 0,
"values": [
0
],
"total": 0,
"matched": 0,
"capped": true
}
}{
"error": {
"code": "string",
"message": "string"
}
}{
"error": {
"code": "string",
"message": "string"
}
}{
"error": {
"code": "string",
"message": "string"
}
}