Skip to content

Public Dataset API

Query answers a bounded JSON result at POST /api/v2/public/tables/{table_id}/query. With a key it returns up to 10,000 rows a page and pages to the end of the table. The same body without a key goes to POST /api/public/tables/{table_id}/query, which returns up to 100 rows a page. Applications and analytical questions use it.

Current answers the whole current Parquet snapshot. Model training, bulk analysis and scheduled local refreshes use it.

Both take a personal mr_use_… key, created under Settings → Access. The key reads any public dataset and your own workspace’s tables. The first request over a public dataset connects it to your workspace, with no step of your own.

mr_use_… is neither the legacy SDK and hosted-table mr_live_… subscription key nor the mr_cli_… device credential. This API refuses both legacy types.

For agents walks the whole surface in copy-paste form: catalog search, dataset, schema, anonymous preview, query, Parquet.

GET /api/v2/public/datasets?q=weather&limit=20

Catalog metadata is public. The reply carries dataset identity, publisher, licence, tables, public schema metadata and canonical URLs. It never names the workspaces that use a dataset.

POST /api/v2/public/tables/{table_id}/query
x-api-key: mr_use_…
content-type: application/json
{
"columns": ["city", "date", "temperature"],
"filters": [
{ "column": "date", "operator": "gte", "value": "2026-08-01" }
],
"order_by": [{ "column": "temperature", "direction": "desc" }],
"limit": 100
}

A successful reply:

{
"schema_version": "mostlyright-public-table-query.v1",
"query_id": "qry_…",
"table": {
"id": "…",
"version_id": "…",
"content_digest": "sha256:…",
"published_at": "2026-08-23T10:00:00Z"
},
"columns": [{ "name": "temperature", "type": "float64" }],
"rows": [{ "temperature": 31.2 }],
"execution": {
"returned_rows": 1,
"scanned_bytes": 184320,
"elapsed_ms": 42,
"truncated": false
}
}

The request is a closed query grammar, not SQL. The API rejects unknown columns, unsupported operators and invalid values before it scans a table.

Every page returns next_cursor. Send it back as cursor with the same columns, filters and order_by. The last page returns null. The cursor includes the window and the immutable version the walk started on, so a walk never mixes two versions of a table.

Operator Value Column types
eq, neq One scalar Any
in An array of 1 to 20 scalars Any
gt, gte, lt, lte One scalar Any
between An array of exactly two scalars, low then high, inclusive at both ends. Neither may be null. Numeric, text, date, timestamp. A boolean column is refused.
contains, starts_with, ends_with One non-empty string Text only. Any other column type is refused.
is_null, is_not_null No value Any

Filters are ANDed. The grammar has no or.

A value matches the column it reads: a number for a numeric column, a boolean for a boolean column, a string for a text column. Date and timestamp columns take ISO strings, 2026-08-01 for a date and 2026-08-01T00:00:00Z for a timestamp. contains, starts_with and ends_with read text columns only, the ones the schema reports as string, and they are case-sensitive.

A range with two bounds is two filters:

{
"columns": ["city", "date", "temperature"],
"filters": [
{ "column": "date", "operator": "gte", "value": "2026-08-01" },
{ "column": "date", "operator": "lt", "value": "2026-09-01" }
],
"limit": 100
}

between says the same range in one filter. It includes both ends, so the upper bound is the last day in the range rather than the first day after it:

{
"columns": ["city", "date", "temperature"],
"filters": [
{ "column": "date", "operator": "between", "value": ["2026-08-01", "2026-08-31"] }
],
"limit": 100
}

aggregates beside group_by answers one row a group.

{
"columns": ["city", "temperature"],
"filters": [
{ "column": "date", "operator": "between", "value": ["2026-08-01", "2026-08-31"] }
],
"group_by": ["city"],
"aggregates": [
{ "function": "avg", "column": "temperature", "as": "mean_temperature" },
{ "function": "count", "as": "readings" }
],
"order_by": [{ "column": "mean_temperature", "direction": "desc" }],
"limit": 25
}

columns stays required and a grouped query does not project it. Give every aggregate an as alias and order by that name. An alias that repeats a group_by column name is refused, and so is an alias two aggregates share. Names are compared without case, because the query engine resolves them that way.

A query groups by at most 4 columns. limit counts groups rather than rows, and truncated says the limit was reached, so there may be more groups. A grouped result has no next page: offset and cursor are refused beside aggregates, and next_cursor is null. Every order_by entry names a group_by column or an aggregate alias, and anything else is refused. Aggregates without group_by answer one row over the whole table and cannot be ordered at all.

The response lists the group columns first in columns, then the aggregates. Each row is an object keyed by group column name and aggregate alias.

GET /api/v2/public/tables/{table_id}/current
x-api-key: mr_use_…

The body is application/vnd.apache.parquet. It carries a strong ETag, Content-Digest, Content-Length and X-Request-Id. Send If-None-Match to skip bytes that have not changed. Use a public dataset has a DuckDB example.

Every JSON error carries schema_version, error and request_id.

Status Meaning
401 The key is missing, invalid, revoked, or the wrong type.
404 The table is unknown, no longer public, or unavailable to this workspace. The three answer identically.
413 The artifact or result is too large. Narrow the query, or take bulk data from Current.
429 Rate limit reached. Honour Retry-After.
503, 504 A temporary service condition. Retry with backoff.

A query has row, byte and time limits. With a key a page holds 10,000 rows and 8 MiB, at 60 queries a minute and 5,000 a day. Without one a page holds 100 rows and 64 KiB, at 30 a minute and 600 a day per address. Execution stops at 15 seconds either way. The reply says when a result was truncated. Read Current into local DuckDB when one request has to return everything.

The machine-readable OpenAPI document is the source for REST request and response schemas. This API takes a workspace mr_use_ key. The legacy SDK and hosted-table API keep mr_live_…. MCP is an OAuth connection onto the same bounded query services, and it never accepts a pasted API key. See Connect an AI tool.