Reference · updated 4 November 2026
REST API
The same data, definitions and query engine your agents reach over MCP, as plain HTTPS and JSON. Use it from scripts, n8n, Make, Zapier or your own backend. Every answer names the dataset version it came from, and every call shows up in the workspace audit.
OpenAPI 3.1 spec https://app.datatomcp.com/v1
Quick start
- In the app, open Account → API keys and create a key. It starts with
dtm_and is shown once. - Find a dataset id with
GET /datasets, or copy it from the dataset page. - Ask for totals. This one uses the team's approved
paid_revenuedefinition:
curl https://app.datatomcp.com/v1/datasets/DATASET_ID/query \
-H "Authorization: Bearer $DATATOMCP_KEY" \
-H "content-type: application/json" \
-d '{
"group_by": ["region"],
"metrics": [{ "metric": "paid_revenue" }, { "fn": "count", "as": "orders" }],
"where": [{ "column": "order_date", "op": "between", "value": ["2026-01-01", "2026-09-30"] }],
"order_by": [{ "column": "paid_revenue", "dir": "desc" }]
}'const res = await fetch("https://app.datatomcp.com/v1/datasets/DATASET_ID/query", {
method: "POST",
headers: { authorization: `Bearer ${process.env.DATATOMCP_KEY}`, "content-type": "application/json" },
body: JSON.stringify({ group_by: ["region"], metrics: [{ metric: "paid_revenue" }] }),
});
const { rows, meta } = await res.json();
// meta.version: pass it as "version" on follow-up calls to stay on the same dataimport os, requests
r = requests.post(
"https://app.datatomcp.com/v1/datasets/DATASET_ID/query",
headers={"Authorization": f"Bearer {os.environ['DATATOMCP_KEY']}"},
json={"group_by": ["region"], "metrics": [{"metric": "paid_revenue"}]},
)
r.raise_for_status()
rows, meta = r.json()["rows"], r.json()["meta"]The answer:
{
"rows": [
{ "region": "EU", "paid_revenue": 412870.5, "orders": 1834 },
{ "region": "US", "paid_revenue": 298114, "orders": 1310 }
],
"meta": {
"dataset_id": "ds_8f2k…",
"version": 14,
"refreshed_at": "2026-11-04T07:12:09.000Z",
"rows_returned": 2,
"rows_matched": 2,
"truncated": false,
"metrics": [{ "name": "paid_revenue", "approved_by": "Ana" }],
"view_url": "https://app.datatomcp.com/app/datasets/ds_8f2k…?q=…"
}
}Authentication
Send the key in the Authorization header: Authorization: Bearer dtm_…. A key acts as the person who created it, with their role in each workspace (owner, editor or viewer). You can limit a key to some workspaces from the Connect page; it then sees nothing else. Keys can't create or revoke other keys: do that in the app. Revoked keys stop working at once.
Agents in Claude, ChatGPT, Cursor and other MCP clients don't need a key: they connect to https://app.datatomcp.com/mcp and sign in with OAuth.
The query
Queries are JSON, not SQL. Column names are checked against the dataset, values are always bound as parameters, and agents get read-only access. There are two modes:
- Rows: leave out
metrics. Useselect,where,order_by,limit(up to 1,000) andoffset. - Totals: add
metrics, andgroup_byfor one row per group.
Filters
{ "column": "status", "op": "in", "value": ["paid", "shipped"] }. Operators: eq, neq, gt, gte, lt, lte, in, not_in, contains, starts_with, is_null, not_null, between. between includes both ends; contains and starts_with ignore case. Call GET /datasets/{id}/values/{column} first to filter on the values that are really there.
Metrics and definitions
A metric is either a calculation, { "fn": "sum", "column": "amount", "as": "revenue" } with fn one of count, count_distinct, sum, avg, min or max, or a saved definition by name: { "metric": "paid_revenue" }. A definition is the team's agreed meaning of a number, approved by a person, and the answer says who approved it. A metric can carry its own where, so "revenue" and "refunded revenue" fit in one call.
Linked datasets
When a dataset links to another (orders to customers, say), the linked columns work as alias.column in select, where and group_by: "group_by": ["customers.segment"]. Chained links work too (orders → customers → regions). GET /datasets/{id} lists them.
Versions
Every refresh makes a new version. Pass "version": 14 on a query to read the data an earlier answer came from, so a follow-up question doesn't change under you. Free keeps 2 versions, Pro 20, for 30 days.
What every answer carries
Datasets
Add, describe, refresh, change and remove datasets.
GET /datasets
List datasets. Datasets of every workspace the key can reach.
| Parameter | In | Type | Description |
|---|---|---|---|
| workspace_id | query | string | Only this workspace. |
POST /datasets
Add a dataset. Upload a CSV or Excel file (raw body or multipart field `file`), or send JSON with a public CSV link or a Google Sheet you shared through the app.
| Parameter | In | Type | Description |
|---|---|---|---|
| workspace_id | query | string | Upload only: the target workspace. |
| sheet | query | string | Upload only: which Excel sheet. |
| Body field | Type | Description | |
|---|---|---|---|
| source_url | string | A public CSV or Excel link (Google Sheets publish link, Dropbox, S3...). | |
| google_sheet | object | ||
| name | string | ||
| workspace_id | string | ||
| sheet | string | ||
Uploads: send the file as the raw body (content-type: text/csv) or as multipart field file. Excel files work the same way.
GET /datasets/{id}
Describe a dataset. Columns with types and examples, the import report, links, definitions and glossary.
| Parameter | In | Type | Description |
|---|---|---|---|
| id | path | string | Dataset id, from GET /datasets. |
PATCH /datasets/{id}
Rename, switch auto-sync or change column types.
| Parameter | In | Type | Description |
|---|---|---|---|
| id | path | string | Dataset id, from GET /datasets. |
| Body field | Type | Description | |
|---|---|---|---|
| name | string | ||
| auto_sync | boolean | Google Sheets and links: sync within minutes (Pro). | |
| column_types | object | Re-imports the data with these types. | |
DELETE /datasets/{id}
Delete a dataset. Owners and editors only. Removes every version.
| Parameter | In | Type | Description |
|---|---|---|---|
| id | path | string | Dataset id, from GET /datasets. |
POST /datasets/{id}/refresh
Refresh from the source. Links and Google Sheets are fetched again. Uploads take the new file as the body, like POST /datasets. Each refresh is a new version.
| Parameter | In | Type | Description |
|---|---|---|---|
| id | path | string | Dataset id, from GET /datasets. |
Uploads: send the file as the raw body (content-type: text/csv) or as multipart field file. Excel files work the same way.
GET /datasets/{id}/docs.md
Dataset docs as Markdown. Columns, definitions and example calls, ready to paste into an agent's instructions.
| Parameter | In | Type | Description |
|---|---|---|---|
| id | path | string | Dataset id, from GET /datasets. |
PUT /datasets/{id}/columns/{column}
Describe a column. Editors only. What the column means, its unit, its role and whether it adds up. Kept across refreshes; agents read it in describe_dataset and the answers' units.
| Parameter | In | Type | Description |
|---|---|---|---|
| id | path | string | Dataset id, from GET /datasets. |
| column | path | string | Column id. |
| Body field | Type | Description | |
|---|---|---|---|
| description | string | ||
| unit | stringnull | ||
| role | id | date | dimension | metric | metric needs a number column. | |
| summable | boolean | ||
DELETE /datasets/{id}/columns/{column}
Back to the drafted meaning.
| Parameter | In | Type | Description |
|---|---|---|---|
| id | path | string | Dataset id, from GET /datasets. |
| column | path | string | Column id. |
Query
Rows and totals, distinct values and versions. One engine shared with the MCP server.
POST /datasets/{id}/query
Query rows or totals. Without metrics: rows. With metrics: totals, per group with group_by. Saved definitions are used as { "metric": "name" }. Every call is recorded in the workspace audit.
| Parameter | In | Type | Description |
|---|---|---|---|
| id | path | string | Dataset id, from GET /datasets. |
| Body field | Type | Description | |
|---|---|---|---|
| select | string[] | Columns to return (rows mode). Leave out for all columns. | |
| where | Filter[] | ||
| group_by | string[] | ||
| grain | day | week | month | quarter | year | Group date and timestamp columns in group_by by this period (weeks start on Monday). Each group is labelled with the period's first day. | |
| metrics | Metric[] | With metrics the query returns totals, per group when group_by is set. | |
| order_by | object[] | ||
| limit | integer | ||
| offset | integer | ||
| version | integer | Pin the query to a dataset version, so follow-up calls read the same data. | |
GET /datasets/{id}/values/{column}
Distinct values of a column. The real values with their counts, so filters match "France" and not "FR".
| Parameter | In | Type | Description |
|---|---|---|---|
| id | path | string | Dataset id, from GET /datasets. |
| column | path | string | Column id. |
| limit | query | integer | |
| version | query | integer | Read an older version still kept (GET /datasets/{id}/versions). |
GET /datasets/{id}/versions
Versions still kept.
| Parameter | In | Type | Description |
|---|---|---|---|
| id | path | string | Dataset id, from GET /datasets. |
Definitions
Saved metrics and the glossary agents read.
GET /datasets/{id}/metrics
Definitions of a dataset.
| Parameter | In | Type | Description |
|---|---|---|---|
| id | path | string | Dataset id, from GET /datasets. |
| drafts | query | 1 | Include drafts waiting for approval. |
PUT /datasets/{id}/metrics/{name}
Add or replace a definition. Editors only. A definition saved over the API is approved by the key's owner.
| Parameter | In | Type | Description |
|---|---|---|---|
| id | path | string | Dataset id, from GET /datasets. |
| name | path | string | snake_case name, like paid_revenue. |
| Body field | Type | Description | |
|---|---|---|---|
| description | string | ||
| fn | count | count_distinct | sum | avg | min | max | ||
| column | string | ||
| where | Filter[] | ||
| unit | string | ||
| aliases | string[] | Other names that answer to this definition, like sales. | |
| direction | higher_is_better | lower_is_better | Guessed from the name when left out (costs and churn: lower is better). | |
DELETE /datasets/{id}/metrics/{name}
Remove a definition.
| Parameter | In | Type | Description |
|---|---|---|---|
| id | path | string | Dataset id, from GET /datasets. |
| name | path | string | snake_case name, like paid_revenue. |
POST /datasets/{id}/metrics/{name}/approve
Approve a draft. Editors only. From then on agents get it, with the approver's name.
| Parameter | In | Type | Description |
|---|---|---|---|
| id | path | string | Dataset id, from GET /datasets. |
| name | path | string | Name of a drafted definition. |
GET /workspaces/{id}/overview
What a workspace can answer. Every dataset with a one-line description, its columns' meanings and freshness; the approved definitions; the links between datasets; the glossary; and five example questions with the exact call that answers each. Same as the MCP workspace_overview tool.
| Parameter | In | Type | Description |
|---|---|---|---|
| id | path | string | Workspace id, from GET /workspaces. |
GET /workspaces/{id}/definitions
All definitions and the glossary of a workspace.
| Parameter | In | Type | Description |
|---|---|---|---|
| id | path | string | Workspace id, from GET /workspaces. |
| drafts | query | 1 | Include drafts waiting for approval. |
Charts
A chart from a query, or from your own chart spec.
POST /datasets/{id}/chart
Chart from a query. Runs the query like /query (same checks, same audit), then draws its rows.
| Parameter | In | Type | Description |
|---|---|---|---|
| id | path | string | Dataset id, from GET /datasets. |
| Body field | Type | Description | |
|---|---|---|---|
| chart | line | area | columns | bar | donut | treemap | waffle | funnel | pareto | scatter | heatmap | histogram | kpi | table | variance | bullet | waterfall | scorecard | combo | radar | candlestick | gauge | box | dotPlot | ||
| x | string | Result column for the x axis or the categories (a group_by column). | |
| y | string[] | Result columns with the values. | |
| series | string | Result column whose values become separate series. | |
| title | string | Write the takeaway, like "EU revenue doubled in Q3". | |
| subtitle | string | ||
| source | string | ||
| note | string | ||
| format | number | percent | currency | compact | ||
| currency | string | ||
| highlight | string[] | Series or categories to highlight; the rest are drawn in grey. | |
| grain | day | week | month | quarter | year | Groups the dates of a time chart by period, and the query's date group columns with them. | |
| mode | stacked | grouped | percent | columns and bar: stacked (default), side by side, or each adding up to 100%. | |
| pie | boolean | donut: draw a full pie. | |
| invert | boolean | Lower is better for the values drawn (costs). Definitions marked lower is better set it for their own columns. | |
| words | object | Replace the words a chart draws by itself, e.g. { "plan": "Budget", "other": "Autres" }. | |
| theme | datatomcp-dark | datatomcp-light | editorial-light | editorial-dark | brand-light | brand-dark | ||
| width | integer | ||
| output | svg | figure | spec | csv | Leave out for JSON with spec, svg and meta. | |
| query | Query | A constrained JSON query. Column names are checked against the schema, values are bound as parameters, and every query has a limit and a timeout. | |
| version | integer | ||
curl https://app.datatomcp.com/v1/datasets/DATASET_ID/chart \
-H "Authorization: Bearer $DATATOMCP_KEY" -H "content-type: application/json" \
-d '{ "chart": "columns", "x": "month", "title": "EU revenue doubled in Q3",
"query": { "group_by": ["month"], "metrics": [{ "metric": "paid_revenue" }] },
"output": "svg" }' > chart.svgPOST /charts/render
Draw your own chart spec. Any of the 52 chart types of @datatomcp/charts from a spec you send. No data access.
| Body field | Type | Description | |
|---|---|---|---|
| spec | object | ||
| theme | datatomcp-dark | datatomcp-light | editorial-light | editorial-dark | brand-light | brand-dark | ||
| width | integer | ||
| output | svg | figure | csv | ||
Dashboards
Stories (slides) and boards (one page) built from queries; refresh re-runs them exactly.
GET /dashboards
List dashboards. The dashboards of a workspace (the current one when workspace_id is left out), with owner, type and last refresh.
| Parameter | In | Type | Description |
|---|---|---|---|
| workspace_id | query | string |
POST /dashboards
Create a dashboard. A story takes slides, each with an optional title and blocks; a board takes blocks. Blocks: chart { dataset_id, query, chart }, kpi { dataset_id, query: { metrics }, title }, text { text }, commentary { prompt } (written from the numbers on your own AI key at each refresh), shape { shape }. Layout is optional: blocks are placed for you. Charts are drawn at once. Only the workspace's own datasets can be used.
| Body field | Type | Description | |
|---|---|---|---|
| workspace_id | string | ||
| type | story | board | ||
| title | string | ||
| subtitle | string | ||
| mode | light | dark | ||
| slides | object[] | ||
| blocks | object[] | ||
GET /dashboards/{id}
Read a dashboard. Its blocks with their drawn chart specs, comments and who is editing what.
| Parameter | In | Type | Description |
|---|---|---|---|
| id | path | string | Dashboard id, from GET /dashboards. |
DELETE /dashboards/{id}
Delete a dashboard. Only the person who made it, or the workspace owner.
| Parameter | In | Type | Description |
|---|---|---|---|
| id | path | string | Dashboard id, from GET /dashboards. |
POST /dashboards/{id}/refresh
Refresh a dashboard. Re-runs every saved query on the latest data, exactly; no AI key needed. Commentary is rewritten on your own AI key (Account → AI models); without one it keeps its last text and the answer says "API key not provided". Every refresh shows in Audit → Dashboard refreshes.
| Parameter | In | Type | Description |
|---|---|---|---|
| id | path | string | Dashboard id. |
Account
Workspaces and usage.
GET /workspaces
Workspaces the key can reach.
GET /usage
Today's usage.
Limits
| Free | Pro | |
|---|---|---|
| Datasets | 3 | 25 |
| Rows per dataset | 100,000 | 2,000,000 |
| File size | 20 MB | 200 MB |
| Queries per day | 1,000 | 50,000 |
| Rows per response | 1,000 | 1,000 |
| Versions kept | 2 | 20 |
Daily limits reset at 00:00 UTC. GET /usage shows today's count. Queries stop after a few seconds; add filters if one times out.
Errors
Errors are JSON with a stable code and a message written to be shown to a person or an agent: { "error": { "code": "unknown_column", "message": "…" } }.
| Code | Status | Meaning |
|---|---|---|
| invalid_request | 400 | The body or a parameter is wrong. The message names the field. |
| invalid_query | 400 | The query doesn't fit the dataset, like a sum of a text column. |
| unknown_column | 400 | A column that isn't in the dataset. The message lists close matches. |
| unauthorized | 401 | No API key, or a revoked one. |
| forbidden | 403 | The key's owner has no access, or the key is limited to other workspaces. |
| not_found | 404 | No such dataset, workspace or definition, or no access to it. |
| upload_too_large | 413 | The file is over your plan's size limit. |
| row_limit | 413 | The file has more rows than your plan allows. |
| query_limit | 429 | Today's query limit is used. It resets at 00:00 UTC. |
| query_timeout | 408 | The query took too long. Add filters or group by fewer columns. |
| fetch_failed | 400 | The source link or sheet couldn't be read. The dataset keeps its last version. |
Something missing or wrong? Write to [email protected]. For agents, see the MCP setup guides; for charts, the chart library.